@avcodes/mi
Advanced tools
+53
-38
| #!/usr/bin/env node | ||
| // mi — minimal autonomous agent CLI. Streams OpenAI chat completions, executes tool calls in a loop. | ||
| /* | ||
| * Import readline for interactive CLI input, fs to read files. Set MI_DIR | ||
| * for tool modules to locate bundled assets. Exit if OPENAI_API_KEY missing. | ||
| */ | ||
| // ── Imports & environment ──────────────────────────────────────────── | ||
| // Node builtins only — no npm deps. These four cover REPL, filesystem, subprocesses, and home directory. | ||
| import { createInterface } from 'readline'; import { readFileSync, existsSync, readdirSync } from 'fs'; import { spawn } from 'child_process'; import { homedir } from 'os'; | ||
| Object.assign(global, { spawn, readFileSync, existsSync, readdirSync, homedir }); const DIR = new URL('.', import.meta.url).pathname; process.env.MI_DIR = DIR; process.env.MI_PATH = new URL(import.meta.url).pathname; if (!process.env.OPENAI_API_KEY && !process.argv.includes('-h')) { console.error('OPENAI_API_KEY required'); process.exit(1); } | ||
| // Globals: tools run in a separate module scope but need fs/spawn — expose via global rather than re-importing. | ||
| // DIR = package root (for tool/skill discovery); MI_DIR/MI_PATH = env vars so tools can locate project assets. | ||
| Object.assign(global, { spawn, readFileSync, existsSync, readdirSync, homedir }); const DIR = new URL('.', import.meta.url).pathname; Object.assign(process.env, { MI_DIR: DIR, MI_PATH: new URL(import.meta.url).pathname }); if (!process.env.OPENAI_API_KEY && !process.argv.includes('-h')) { console.error('OPENAI_API_KEY required'); process.exit(1); } | ||
| /* Discover and load tools from tools/ directory. Each module default-exports {name, description, parameters, handler}. */ | ||
| const toolMods = await Promise.all(readdirSync(DIR + 'tools').filter(f => f.endsWith('.mjs')).map(f => import(DIR + 'tools/' + f))), T = toolMods.map(m => m.default), dim = s => `\x1b[90m${s}\x1b[0m`, { listSkills } = toolMods.find(m => m.listSkills); | ||
| const tools = Object.fromEntries(T.map(t => [t.name, t.handler])), toolsDef = T.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters } })); | ||
| // ── Tool discovery ─────────────────────────────────────────────────── | ||
| // Load tool modules; each exports {name, description, parameters, handler}. | ||
| // ANSI helpers: 90 = bright black (gray), 31 = red (error), 38;5;208 = orange (brand) | ||
| const gray = s => `\x1b[90m${s}\x1b[0m`, red = s => `\x1b[31m${s}\x1b[0m`, orange = s => `\x1b[38;5;208m${s}\x1b[0m`; | ||
| let tools, toolSchemas, listSkills, loadId = 0; async function loadTools() { const toolMods = await Promise.all(readdirSync(`${DIR}tools`).filter(file => file.endsWith('.mjs')).map(file => import(`${DIR}tools/${file}?v=${++loadId}`))), defs = toolMods.map(mod => mod.default); tools = Object.fromEntries(defs.map(def => [def.name, def.handler])); toolSchemas = defs.map(def => ({ type: 'function', function: { name: def.name, description: def.description, parameters: def.parameters } })); listSkills = toolMods.find(mod => mod.listSkills)?.listSkills; } await loadTools(); | ||
| /* | ||
| * Call the chat API in a loop, executing tool calls, until the model | ||
| * returns a plain text reply. Streams content tokens to stdout as they arrive. | ||
| */ | ||
| // ── Agent loop: chat → stream → execute tools → repeat ────────────── | ||
| // Streams the API response, executes any tool calls, and loops until the | ||
| // model returns a plain text reply (no further tool invocations). | ||
| async function run(messages) { while (true) { | ||
| /* POST with stream:true; throw on non-200 by reading the JSON error body. */ | ||
| const response = await fetch(`${(process.env.OPENAI_BASE_URL || 'https://api.openai.com').replace(/\/+$/, '')}/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, tools: toolsDef, stream: true }) }); if (!response.ok) { const error = await response.json().catch(()=>({})); throw new Error(error.error?.message || `HTTP ${response.status}`); } | ||
| // ─ Send streaming chat completion request ─ | ||
| await loadTools(); const response = await fetch(`${(process.env.OPENAI_BASE_URL || 'https://api.openai.com').replace(/\/+$/, '')}/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, tools: toolSchemas, stream: true }) }); | ||
| if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error?.message || `HTTP ${response.status}`); } | ||
| /* Iterate SSE deltas: write content tokens to stdout, merge tool_call fragments by index into one assistant message. */ | ||
| const message = { role: 'assistant', content: '' }, dec = new TextDecoder(); let buf = ''; | ||
| for await (const chunk of response.body) { buf += dec.decode(chunk, {stream:true}); let sep; while ((sep = buf.indexOf('\n\n')) >= 0) { const ev = buf.slice(0, sep); buf = buf.slice(sep+2); for (const line of ev.split('\n')) { if (!line.startsWith('data: ')) continue; const data = line.slice(6); if (data === '[DONE]') continue; let json; try { json = JSON.parse(data); } catch { continue; } if (json.error) throw new Error(json.error.message || JSON.stringify(json.error)); const delta = json.choices?.[0]?.delta; if (!delta) continue; if (delta.content) { process.stdout.write(delta.content); message.content += delta.content; } if (delta.tool_calls) { message.tool_calls ||= []; for (const toolDelta of delta.tool_calls) { const merged = message.tool_calls[toolDelta.index] ||= { id:'', type:'function', function:{name:'',arguments:''} }; if (toolDelta.id) merged.id = toolDelta.id; if (toolDelta.type) merged.type = toolDelta.type; if (toolDelta.function?.name) merged.function.name += toolDelta.function.name; if (toolDelta.function?.arguments) merged.function.arguments += toolDelta.function.arguments; } } } } } | ||
| if (message.content) process.stdout.write('\n'); messages.push(message); if (!message.tool_calls) return; | ||
| // ─ Parse SSE stream: print content tokens, accumulate tool-call deltas by index ─ | ||
| // SSE frames are delimited by double newlines (\n\n). We buffer raw bytes and split on | ||
| // that boundary, then extract the JSON after each "data: " prefix (per SSE spec). | ||
| const message = { role: 'assistant', content: '' }, decoder = new TextDecoder(); let buffer = ''; | ||
| // Tool-call deltas arrive as fragments across multiple SSE events, each keyed by tc.index. | ||
| // We merge them into `slot` objects: IDs and types overwrite, but name and arguments strings | ||
| // are *appended* because the API streams them in pieces (e.g. arguments may arrive as | ||
| // '{"com' then 'mand": "ls"}'). The completed slots form the tool_calls array for execution. | ||
| for await (const chunk of response.body) { buffer += decoder.decode(chunk, { stream: true }); let pos; while ((pos = buffer.indexOf('\n\n')) >= 0) { const event = buffer.slice(0, pos); buffer = buffer.slice(pos + 2); /* skip past \n\n delimiter */ | ||
| for (const line of event.split('\n')) { if (!line.startsWith('data: ')) continue; const payload = line.slice(6); /* strip "data: " prefix */ if (payload === '[DONE]') continue; let json; try { json = JSON.parse(payload); } catch { continue; } if (json.error) throw new Error(json.error.message || JSON.stringify(json.error)); const delta = json.choices?.[0]?.delta; /* single choice; we never request n>1 */ if (!delta) continue; if (delta.content) { process.stdout.write(delta.content); message.content += delta.content; } | ||
| if (delta.tool_calls) { message.tool_calls ||= []; for (const tc of delta.tool_calls) { const slot = message.tool_calls[tc.index] ||= { id: '', type: 'function', function: { name: '', arguments: '' } }; if (tc.id) slot.id = tc.id; if (tc.type) slot.type = tc.type; const fn = tc.function; if (fn?.name) slot.function.name += fn.name; if (fn?.arguments) slot.function.arguments += fn.arguments; } } } } } if (message.content) process.stdout.write('\n'); messages.push(message); if (!message.tool_calls) return; | ||
| for (const toolCall of message.tool_calls) { | ||
| const {name} = toolCall.function, args = JSON.parse(toolCall.function.arguments); | ||
| // ─ Execute each tool call and push results back into history ─ | ||
| for (const toolCall of message.tool_calls) { const { name, arguments: rawArgs } = toolCall.function, args = JSON.parse(rawArgs); | ||
| console.log(gray(`⟡ ${name}(${JSON.stringify(args)})`)); if (!tools[name]) { messages.push({ role: 'tool', tool_call_id: toolCall.id, content: `Error: unknown tool "${name}". Available: ${Object.keys(tools).join(', ')}` }); continue; } const result = String(await tools[name](args)); | ||
| // Log truncated to 200 chars for terminal readability; the model gets the full result. | ||
| console.log(gray(result.length > 200 ? `${result.slice(0, 200)}…` : result)); messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result }); } } } | ||
| /* Log the call, run the tool, log a truncated result, push to history. */ | ||
| 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)); messages.push({ role: 'tool', tool_call_id: toolCall.id, content: out }); | ||
| // ── System prompt ──────────────────────────────────────────────────── | ||
| // SYSTEM_PROMPT env var fully overrides the built-in default (ternary, not merge). | ||
| // CWD and date are always appended so the model knows where it is and when. | ||
| const DEFAULT_PROMPT = 'You are mi, an autonomous coding agent in a raw terminal. The user sees a transcript, not a chat UI. There is no markdown renderer: avoid headings, bullets, tables, bold, inline-code styling, and code fences unless the user explicitly asks or code is the product. Write lowercase prose unless code, paths, proper nouns, or quoted output require case.\n\nPersona: quiet, mechanical, precise. Short plain sentences. Present tense. No filler, no hedging, no cheerleading, no obvious narration. Do not say "I can", "I will", "let me", "we need", "probably", "should", or "happy to". Before a tool call, write at most one status line under 8 words: "checking files.", "running tests.", "writing parser." After tool results, say only the observed fact or the next action. If a plan helps, make it at most 3 short lines.\n\nAct rather than speculate. Explore, execute one step at a time, verify before moving on. If something fails, read the error, form a concrete diagnosis, change approach, retry. Keep going until the task is complete. Do not explain shell basics, tool mechanics, or your reasoning unless asked. Do not fake tool output; the harness prints real tool calls and results.\n\nMinimize context usage when reading files: head -20 for starts, tail -20 for ends, sed -n \'10,30p\' for ranges, grep -n to locate then read around matches. Reserve cat for short files. Edit with sed -i or heredocs (cat > file <<\'EOF\'). Always read before editing. You may write new tools in tools/*.mjs; they hot-load before the next model call. When a request matches a skill description below, load that skill and follow it.\n\nFinal answer: 1-5 short lines. Lead with what changed or what you found. Include the proof command/output that matters. No recap of every step.'; | ||
| const SYSTEM = (process.env.SYSTEM_PROMPT || DEFAULT_PROMPT) + `\nCWD: ${process.cwd()}\nDate: ${new Date().toISOString()}`; | ||
| } } } | ||
| // ── CLI setup: history, flags, context injection ───────────────────── | ||
| // getArg: returns the value after a flag (e.g. getArg('-p') → prompt string), or false if absent. | ||
| // Uses short-circuit: indexOf returns -1 when missing, so `i >= 0 && argv[i + 1]` is false without a flag. | ||
| const history = [{ role: 'system', content: SYSTEM }], getArg = key => { const i = process.argv.indexOf(key); return i >= 0 && process.argv[i + 1]; }; | ||
| /* System prompt: built-in instructions plus current directory and date. */ | ||
| const SYSTEM = (process.env.SYSTEM_PROMPT || 'You are mi, an autonomous agent. You run in a raw terminal—no markdown renderer, so avoid **, `, #, and ```. Use whitespace and plain punctuation. Be concise.\n\nAct rather than speculate. Explore the problem, form a plan, execute one step at a time, verify each step before proceeding. If something fails, diagnose and retry. Keep going until the task is complete. When a request matches a skill description below, load that skill and follow it.\n\nMinimize context usage when reading files: head -20 for starts, tail -20 for ends, sed -n \'10,30p\' for ranges, grep -n to locate then read around matches. Reserve cat for short files. Edit with sed -i or heredocs (cat > file <<\'EOF\'). Always read before editing.\n\nWhen done, show the command output that proves it—not a summary.') + `\nCWD: ${process.cwd()}\nDate: ${new Date().toISOString()}`; | ||
| /* History seeded with the system prompt; getArg reads a named CLI flag. */ | ||
| const history = [{ role: 'system', content: SYSTEM }], getArg = key => (idx => idx >= 0 && process.argv[idx + 1])(process.argv.indexOf(key)); | ||
| 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\nbash tool args: timeout=<ms> kills after delay · bg=truthy detaches and returns pid+log'); process.exit(0); } | ||
| /* Prepend -f file, AGENTS.md, and the skills index (if present) to the system message. */ | ||
| 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'); const skills = listSkills(); if (skills.length) history[0].content += '\n\nSkill descriptions:\n' + skills.join('\n'); | ||
| // Append -f file contents, AGENTS.md (auto-ingested repo context), and skill summaries to system message. | ||
| const sysMsg = history[0], fileArg = getArg('-f'); if (fileArg) sysMsg.content += `\n\nFile (${fileArg}):\n${readFileSync(fileArg, 'utf8')}`; if (existsSync('AGENTS.md')) sysMsg.content += `\n${readFileSync('AGENTS.md', 'utf8')}`; const skills = listSkills(); if (skills.length) sysMsg.content += `\n\nSkill descriptions:\n${skills.join('\n')}`; | ||
| if (getArg('-p')) { history.push({ role: 'user', content: getArg('-p') }); await run(history); process.exit(0); } | ||
| // ── One-shot modes: -p flag and stdin pipe ─────────────────────────── | ||
| const prompt = getArg('-p'); if (prompt) { history.push({ role: 'user', content: prompt }); await run(history); process.exit(0); } if (!process.stdin.isTTY) { let input = ''; for await (const chunk of process.stdin) input += chunk; /* Buffer auto-coerces to string via += */ history.push({ role: 'user', content: input.trim() }); await run(history); process.exit(0); } | ||
| if (!process.stdin.isTTY) { let inputStr = ''; for await (const chunk of process.stdin) inputStr += chunk; history.push({ role: 'user', content: inputStr.trim() }); await run(history); process.exit(0); } | ||
| // ── Interactive REPL ───────────────────────────────────────────────── | ||
| // readline setup, version banner, then an infinite prompt loop | ||
| const readLine = createInterface({ input: process.stdin, output: process.stdout }); const promptUser = query => new Promise(resolve => readLine.question(query, resolve)); const version = JSON.parse(readFileSync(`${DIR}package.json`, 'utf8')).version; console.log(`${orange('◰ mi')}${gray(`/${version}`)}`); | ||
| /* Set up the readline interface and enter the interactive REPL. */ | ||
| const readLine = createInterface({ input: process.stdin, output: process.stdout }); const promptUser = query => new Promise(resolve => readLine.question(query, resolve)); const ver = JSON.parse(readFileSync(DIR+'package.json','utf8')).version; console.log('\x1b[38;5;208m◰ mi\x1b[90m/'+ver+'\x1b[0m'); | ||
| readLine.on('close', () => process.exit(0)); while (true) { const input = await promptUser('\n> '); if (input === '/reset') { history.splice(1); console.log(dim('✓ reset')); continue; } if (input.trim()) { history.push({ role: 'user', content: input }); process.stdout.write(dim('─────')+'\n'); try { await run(history); } catch(error) { console.error('\x1b[31m✗ ' + error.message + '\x1b[0m'); history.pop(); } } } | ||
| // Ctrl-D (EOF) → clean exit; then loop: read input → run agent → repeat | ||
| // /reset: keep system prompt (index 0), drop all conversation history | ||
| // Error recovery: pop the failed user message so the model never sees it | ||
| readLine.on('close', () => process.exit(0)); while (true) { const input = await promptUser('\n> '); if (input === '/reset') { history.splice(1); /* keep system prompt at [0] */ console.log(gray('✓ reset')); continue; } if (input.trim()) { history.push({ role: 'user', content: input }); process.stdout.write(`${gray('─────')}\n`); try { await run(history); } catch (error) { console.error(red(`✗ ${error.message}`)); history.pop(); } } } |
+1
-1
| { | ||
| "name": "@avcodes/mi", | ||
| "version": "1.6.1", | ||
| "version": "1.6.2", | ||
| "description": "agentic coding in 30 loc. a loop, two tools, and an llm.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+2
-2
@@ -13,3 +13,3 @@  | ||
| - bundled skills: `plan`, `tasks`, `delegate`, `explore`, `refactor`, `review`, `verify`, `debug`, `tdd`, `new-skill`, `self` | ||
| - modular tools: add new tools by dropping `.mjs` files in `tools/` (auto-discovered at startup) | ||
| - modular tools: add new tools by dropping `.mjs` files in `tools/` (hot-loaded before each model call) | ||
| - self-extending: agent can write its own tools via the `self` skill | ||
@@ -79,3 +79,3 @@ - recursive agents: tools can spawn sub-agents by calling `mi` as a child process | ||
| the harness auto-discovers tools at startup by scanning `tools/*.mjs`. two tools ship by default: | ||
| the harness hot-loads tools before each model call by scanning `tools/*.mjs`. two tools ship by default: | ||
@@ -82,0 +82,0 @@ - `bash` gives the agent access to the entire system: git, curl, compilers, package managers, and file I/O (via `cat`, `sed -n`, `sed -i`, heredocs; the system prompt teaches the patterns). optional `timeout=<ms>` kills the process after the given delay and resolves with `[timeout]`. optional `bg=truthy` runs the command detached and returns `pid:X log:/tmp/mi-*.log` immediately. |
@@ -13,3 +13,3 @@ --- | ||
| - `$MI_PATH` — the main harness file. | ||
| - `$(dirname $MI_PATH)/tools/*.mjs` — tool modules (bash, skill), auto-discovered at startup. | ||
| - `$(dirname $MI_PATH)/tools/*.mjs` — tool modules (bash, skill), hot-loaded before each model call. | ||
| - `$(dirname $MI_PATH)` — the package root: `README.md`, `package.json`, `AGENTS.md`, `skills/`, `tools/`, `tests/`, `scripts/`. | ||
@@ -67,3 +67,3 @@ - `$(dirname $MI_PATH)/skills/<name>/SKILL.md` — bundled skills. | ||
| ``` | ||
| 3. Restart `mi` — tools are discovered at startup. | ||
| 3. Continue the conversation — tools hot-load before the next model call. | ||
| 4. Test by asking for the tool to be used. | ||
@@ -70,0 +70,0 @@ |
+14
-3
@@ -0,4 +1,15 @@ | ||
| // tools/bash.mjs — Shell execution tool: foreground (captured) and background (detached) modes | ||
| export default { name: 'bash', description: 'Runs in a detached process group. Returns combined stdout+stderr. Optional: timeout=ms kills after delay; bg=truthy fully detaches and returns pid + log file path.', parameters: { type: 'object', properties: { command: { type: 'string' }, timeout: { type: 'string' }, bg: { type: 'string' } }, required: ['command'] }, handler: ({command, timeout, bg}) => { | ||
| if (bg) { const log = `/tmp/mi-${Date.now()}.log`; const proc = spawn('bash', ['-c', `${command} >${log} 2>&1`], { stdio: 'ignore', detached: true }); proc.unref(); return `pid:${proc.pid} log:${log}`; } | ||
| return new Promise(resolve => { const child = spawn('bash', ['-c', command], { stdio: ['ignore', 'pipe', 'pipe'], detached: true }); let output = ''; child.stdout.on('data', data => output += data); child.stderr.on('data', data => output += data); const cleanup = () => { try { process.kill(-child.pid) } catch (err) {} }; process.on('SIGINT', cleanup); const timer = timeout ? setTimeout(() => { cleanup(); resolve(output + '\n[timeout]') }, +timeout) : null; child.on('exit', () => { process.off('SIGINT', cleanup); if (timer) clearTimeout(timer); resolve(output); }); }); | ||
| }}; | ||
| // ── Background mode: fire-and-forget ────────────────────────────── | ||
| // Redirect stdout+stderr to a log file so the caller can tail it later. | ||
| // unref() lets the Node process exit without waiting for the child. | ||
| if (bg) { const logFile = `/tmp/mi-${Date.now()}.log`; const child = spawn('bash', ['-c', `${command} >${logFile} 2>&1`], { stdio: 'ignore', detached: true }); child.unref(); return `pid:${child.pid} log:${logFile}`; } | ||
| // ── Foreground mode: capture output, respect timeout, clean up ──── | ||
| // detached: true creates a new process group so we can kill the entire tree via negative pid. | ||
| // killGroup uses try/catch because the process group may already be dead. | ||
| // SIGINT wired so Ctrl-C in the terminal kills the child group, not just mi. | ||
| // On exit: detach SIGINT handler to avoid leaking listeners, cancel timer. | ||
| return new Promise(resolve => { const child = spawn('bash', ['-c', command], { stdio: ['ignore', 'pipe', 'pipe'], detached: true }); let output = ''; for (const stream of [child.stdout, child.stderr]) stream.on('data', chunk => output += chunk); /* Buffer auto-coerces to string via += */ | ||
| const killGroup = () => { try { process.kill(-child.pid); } catch {} }; process.on('SIGINT', killGroup); const timer = timeout ? setTimeout(() => { killGroup(); resolve(`${output}\n[timeout]`) }, +timeout) : null; child.on('exit', () => { process.off('SIGINT', killGroup); if (timer) clearTimeout(timer); resolve(output); }); }); }}; |
+13
-2
@@ -1,3 +0,14 @@ | ||
| const meta = s => ({ name: s.match(/^name:\s*(.+)$/m)?.[1], description: s.match(/^description:\s*(.+)$/m)?.[1] || '' }), getSkillDirs = () => [`${process.env.MI_DIR}skills/`, `${process.env.HOME || homedir()}/.agents/skills/`], loadSkill = name => { for (const dir of getSkillDirs()) if (existsSync(dir + name + '/SKILL.md')) return readFileSync(dir + name + '/SKILL.md', 'utf8'); }; | ||
| export const listSkills = () => getSkillDirs().flatMap(dir => existsSync(dir) ? readdirSync(dir).filter(dirName => existsSync(dir + dirName + '/SKILL.md')).map(dirName => { const { name, description } = meta(readFileSync(dir + dirName + '/SKILL.md', 'utf8')); return `- ${name || dirName}: ${description}`; }) : []); | ||
| // tools/skill.mjs — Skill discovery and loading: find SKILL.md files, parse frontmatter, list or read them | ||
| // ── Frontmatter parsing & skill resolution ────────────────────────── | ||
| // Regex: ^name:\s*(.+)$ with /m flag — anchors match per-line so we find "name:" at any line start, | ||
| // skip whitespace after the colon, and capture everything to EOL. Optional chaining returns undefined | ||
| // if the field is missing (not all SKILL.md files include both fields). | ||
| const parseFrontmatter = content => ({ name: content.match(/^name:\s*(.+)$/m)?.[1], description: content.match(/^description:\s*(.+)$/m)?.[1] || '' }), skillDirs = () => [`${process.env.MI_DIR}skills/`, `${process.env.HOME || homedir()}/.agents/skills/`], loadSkill = name => { const path = skillDirs().map(dir => `${dir}${name}/SKILL.md`).find(existsSync); return path && readFileSync(path, 'utf8'); }; | ||
| // ── Skill listing ─────────────────────────────────────────────────── | ||
| // Scan all skill directories; return "- name: description" bullets for each valid SKILL.md found | ||
| export const listSkills = () => skillDirs().flatMap(dir => existsSync(dir) ? readdirSync(dir).filter(entry => existsSync(`${dir}${entry}/SKILL.md`)).map(entry => { const { name, description } = parseFrontmatter(readFileSync(`${dir}${entry}/SKILL.md`, 'utf8')); return `- ${name || entry}: ${description}`; }) : []); | ||
| // ── Tool export ───────────────────────────────────────────────────── | ||
| export default { name: 'skill', description: 'With name: returns that skill\'s SKILL.md body. Without name: lists available skills with descriptions.', parameters: { type: 'object', properties: { name: { type: 'string' } }, required: [] }, handler: ({name}) => name ? loadSkill(name) : listSkills().join('\n') }; |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
62314
10.52%103
66.13%1
-50%11
-8.33%