Package Exports
- @semalt-ai/code
- @semalt-ai/code/internals
- @semalt-ai/code/package.json
Readme
Semalt.AI Code CLI
@semalt-ai/code is a self-hosted AI coding assistant for the terminal.
It provides an interactive chat interface, one-shot code generation, AI-assisted file editing, shell command execution, and an agent loop that can read files, write files, and run commands after user approval.
Features
- Interactive terminal chat mode
- OpenAI-compatible API integration
- Streaming responses with terminal-friendly formatting
- Optional reasoning stream display when the backend provides it
- User-approved shell, file read, and file write actions
- File and directory context loading
- One-shot code task mode
- AI-assisted file editing mode
- Model discovery from the configured API
Requirements
- Node.js
>=18(Node 16 is end-of-life;node --testis unreliable on it) - An OpenAI-compatible API endpoint
The default configuration expects a local API server at http://127.0.0.1:8800.
The CLI accepts api_base in either of these forms:
http://127.0.0.1:8800http://127.0.0.1:8800/v1
Both formats are normalized automatically.
Installation
Install the package globally so the semalt-code command is available system-wide.
npm install -g @semalt-ai/codeAfter global installation, run:
semalt-codeInitial Setup
Create the CLI config:
semalt-code init --api-base http://127.0.0.1:8800 --api-key any --dashboard-url https://cli.semalt.ai --default-model defaultThis writes configuration to:
~/.semalt-ai/config.jsonExample config:
{
"api_base": "http://127.0.0.1:8800",
"api_key": "any",
"dashboard_url": "https://cli.semalt.ai",
"auth_token": "",
"default_model": "default",
"temperature": 0.7,
"request_timeout_ms": 900000,
"stream": true
}You can also set "api_base" to a URL that already ends with /v1.
Usage
semalt-code [command] [options]Commands
semalt-codesemalt-code chatStarts interactive chat mode.semalt-code code <prompt>Runs a one-shot coding task.semalt-code edit <file> <instruction>Sends a file to the model and overwrites it with the returned result.semalt-code shell <command>Runs a shell command with approval prompts.semalt-code loginStarts browser-based login and stores the confirmed CLI token in~/.semalt-ai/config.json.semalt-code whoamiShows the current user associated with the saved CLI auth token.semalt-code logoutLogs out the current CLI user and clears the saved local auth token.semalt-code modelsLists your models from the dashboard and lets you choose the current one for the CLI.semalt-code models addOpens an interactive flow to add an API base URL, API key, and model ID as a reusable model profile.semalt-code initCreates or updates the local config file.semalt-code mcp <list|status|add|remove|auth>Manage MCP servers.addregisters a server (stdiocommand/argsor remote--url),statusconnects and reports each server's tools,authruns the OAuth flow for a remote server (tokens are stored in the OS keychain). Discovered tools register asmcp__<server>__<tool>and run through the same approval-gated agent loop as built-ins — MCP results are treated as untrusted external content, and MCP tools require approval by default (opt in per server withallow/allowAll). In chat,/mcpshows the same status.
Options
-m, --model <name>Override the model name.-v, --versionPrint the current CLI version.-f, --file <path>Load one or more files or directories into the prompt context forcode.-a, --analyzeAftershell, ask the model to summarize the command output.--dry-runForedit, show the generated result without saving it.--api-base <url>Set the API base URL duringinit.--api-key <key>Set the API key duringinit.--default-model <name>Set the default model duringinit.
Interactive Chat Mode
Running semalt-code without arguments starts the terminal chat UI.
Available interactive commands:
/help/file <path>/whoami/logout/model/login/model <name>/models/clear/compact/cost/shell <cmd>!<cmd>/approve/configexit
What /file does
- If you pass a file, its full content is added to the conversation context.
- If you pass a directory, the CLI recursively loads up to 50 non-hidden files.
Agent Behavior
The assistant is instructed to use special tool-like tags:
<exec>...</exec>for shell commands<read_file>...</read_file>for file reads<write_file path="...">...</write_file>for file writes
When the model emits these actions, the CLI:
- Detects them in the response
- Prompts the user for approval
- Executes the action
- Sends the result back to the model
- Continues for up to 125 iterations
This makes the tool behave like a lightweight terminal agent while keeping the user in control.
Examples
Start chat
semalt-codeAsk for a coding task with file context
semalt-code code -f package.json -f index.js "Explain this project and suggest improvements"Edit a file with AI
semalt-code edit index.js "Refactor duplicated logic into helper functions"Preview an edit without saving
semalt-code edit index.js "Add better error handling" --dry-runRun and analyze a shell command
semalt-code shell -a "npm test"List models
semalt-code modelsAdd a saved model profile
semalt-code models addThe CLI will ask for:
- API Base URL
- API Key
- Model ID
Each saved profile is appended to the profile list.
Saved profiles can then be selected inside chat mode with /model or /models.
Show the current version
semalt-code --versionEmbedding SDK
@semalt-ai/code can be embedded in another program as a library, not just run as a
CLI. There are two tiers, physically separated by the package exports map:
| Import | Surface | Stability |
|---|---|---|
require('@semalt-ai/code') |
createAgent — the high-level facade |
Stable (semver) |
require('@semalt-ai/code/internals') |
createAgentRunner, createApiClient, the registries, … |
Unstable — no guarantee, may change in any release |
Both subpaths work for require and import (the package is CommonJS; ESM consumers
get the named exports via interop).
The facade
const { createAgent } = require('@semalt-ai/code');
const agent = createAgent({
apiBase: 'http://127.0.0.1:8800',
apiKey: process.env.SEMALT_API_KEY,
model: 'my-model',
// permission policy — see below
});
const res = await agent.run('Summarise README.md in three bullets');
// res = { result, toolCalls, usage, cost, stopReason, verifyStatus, messages }
console.log(res.result);
await agent.close(); // REQUIRED — releases MCP connections / processesrun(prompt, opts?) executes a prompt to completion and returns the same structured
envelope headless mode produces, plus messages so you can continue the conversation
(agent.run(next, { messages: res.messages })). Stream events with
agent.on('token' | 'assistant' | 'tool' | 'tool-start' | 'error' | 'warning' | 'done', cb).
Permission policy — safe by default
There is no terminal in embedded use, so the facade takes a programmatic permission policy. With neither a policy provided, the default is to refuse every mutating / effectful tool (read-only tools still run) — it never auto-approves:
// 1) an async approver callback
createAgent({ /* … */, approve: async (call) => {
// call = { actionType, description, tag, rule }
return call.tag !== 'delete_file'; // your decision
}});
// 2) preset allow/deny/ask rules (the same engine as the CLI's per-pattern rules)
createAgent({ /* … */, rules: [
{ tool: 'write_file', path: 'src/**', action: 'allow' },
{ tool: 'shell', pattern: 'git *', action: 'allow' },
{ tool: 'shell', pattern: '/curl.*\\| *sh/', action: 'deny' },
]});
// 3) coarse tiers (like --allow-fs/exec/net) and read-only
createAgent({ /* … */, allow: ['fs', 'net'], readonly: true });The OS sandbox and the destructive-command deny-list stay on regardless of
there being no TTY. The sandbox is opt-out only via explicit config
(sandbox: { mode: 'off' }); a host can permit running a command unsandboxed when the
kernel primitive is missing by supplying onUnsandboxed. A deny rule and the
deny-list are honored even under the deliberate dangerouslySkipPermissions: true
gate opt-out.
By default the SDK does not read your ~/.semalt-ai/config.json (a server wants
isolation, not the operator's personal defaults) — pass loadUserConfig: true to layer
it in. Pass arbitrary config under config: { … }.
Lifecycle & multi-instance
Always call
await agent.close()when done. It disconnects MCP servers and frees handles.run()afterclose()throws.Each
createAgentinstance keeps its own config — two instances never share config state.Process-global state (documented limitation). A few things are process-wide, not per-instance, because they were built for the single-process CLI:
- the dynamic tool registry (MCP +
spawn_agent) is global — two instances with different MCP servers would see each other's tools; - file-path confinement (
isPathSafe) and the deny-list/secret/config guards readprocess.cwd()andprocess.argvonce at module load, so they're shared by all instances and the deny-list opt-out requires launching the host process with--dangerously-skip-permissions; - the stdout-chrome-suppression flag is process-wide.
For most embeddings (one agent per process, or instances sharing a CWD and MCP set) none of this matters; run fully-isolated agents in separate processes if it does.
- the dynamic tool registry (MCP +
A runnable example lives in examples/embed.js.
How Responses Are Rendered
The CLI formats streamed output for terminal readability:
- headings
- bullet lists
- numbered lists
- fenced code blocks
- diff-like output
- inline code and file paths
If the backend returns reasoning_content, the CLI also shows a lightweight thinking section during streaming.
Dependency Policy
This project keeps its runtime dependency surface minimal, vetted, and pinned. It
ran with zero runtime dependencies through its first phases; as of v1.9.0 it has a
single one — the official Model Context Protocol SDK,
@modelcontextprotocol/sdk —
adopted to implement MCP against its reference (rather than hand-rolling the protocol).
The policy for any runtime dependency:
- Minimal & justified — added only when a Node.js built-in genuinely cannot do the job, with a recorded rationale.
- Pinned to an exact version — no
^/~ranges inpackage.json. Upgrades are deliberate, reviewed commits. - Reviewed with the lockfile —
package-lock.jsonis committed; adding or bumping a dependency is a reviewed change.
Supply-chain checks. CI runs npm ci (lockfile integrity) and
npm audit --omit=dev --audit-level=high (fails on HIGH/CRITICAL advisories in the
runtime tree). The full audit-findings policy is documented in CLAUDE.md.
The SDK is ESM-only while this project is CommonJS, so it is loaded in exactly one
place — lib/mcp/boundary.js, via dynamic import() — and the rest of the codebase
stays CommonJS.
Notes and Limitations
- It uses Node's built-in
httpandhttpsmodules for all networking; the only runtime dependency is the MCP SDK (see Dependency Policy above). - The
editcommand writes the model output directly back to the target file, so review prompts and backend behavior carefully. - Shell and file operations are approval-based, but they still execute on the local system after approval.
Not yet implemented
A few capabilities are intentionally absent today — documented here so you don't build
on something that isn't wired up. See Deferred / Not Yet Implemented in CLAUDE.md
for the full list and roadmap status.
- MCP tools are interactive-chat only — they are not connected in the
code/edit/shellone-shot commands or headless-p/--printmode. - No session auto-resume — there's no "resume your last session?" prompt at startup;
use
/history(local sessions) or--resume <chat-id>(dashboard chats). - Proxy env vars are not consumed —
HTTPS_PROXY/HTTP_PROXYare read into config but outbound HTTP does not yet route through a proxy agent (matters on corporate networks). - Planned for Phase 4+: per-pattern permissions, self-verification, checkpoints/rewind, and an OS sandbox.
Contributing
PRs must pass the CI pipeline (npm ci + npm audit + lint + tests on Linux/macOS/Windows, Node 18 & 20) before they can be merged. Run npm ci && npm run lint && npm test locally first. Any dependency change must follow the Dependency Policy above (exact pin, committed lockfile, justification).
License
MIT