Package Exports
- @semalt-ai/code
- @semalt-ai/code/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@semalt-ai/code) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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