@semalt-ai/code 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/ARCHITECTURE.md +99 -0
- package/CLAUDE.md +349 -0
- package/README.md +16 -2
- package/index.js +79 -7
- package/lib/agent.js +508 -39
- package/lib/api.js +347 -77
- package/lib/args.js +34 -0
- package/lib/audit.js +31 -0
- package/lib/commands.js +1018 -183
- package/lib/config.js +68 -5
- package/lib/constants.js +58 -0
- package/lib/context.js +2 -6
- package/lib/metrics.js +94 -0
- package/lib/permissions.js +180 -49
- package/lib/prompts.js +89 -13
- package/lib/storage.js +96 -0
- package/lib/tools.js +896 -35
- package/lib/ui/ansi.js +64 -0
- package/lib/ui/chat-history.js +217 -0
- package/lib/ui/create-ui.js +474 -0
- package/lib/ui/diff.js +243 -0
- package/lib/ui/input-field.js +1176 -0
- package/lib/ui/layout.js +53 -0
- package/lib/ui/legacy.js +130 -0
- package/lib/ui/status-bar.js +130 -0
- package/lib/ui/stream.js +158 -0
- package/lib/ui/utils.js +45 -0
- package/lib/ui.js +42 -598
- package/package.json +1 -1
- package/path +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(grep -oP '.{0,120}chat:stash.{0,120}' /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js)",
|
|
5
|
+
"Bash(grep -oP '.{0,100}externalEditor.{0,100}' /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js)"
|
|
6
|
+
]
|
|
7
|
+
}
|
|
8
|
+
}
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# semalt-code — Architecture Reference
|
|
2
|
+
|
|
3
|
+
## lib/ File Responsibilities
|
|
4
|
+
|
|
5
|
+
| File | Responsibility |
|
|
6
|
+
|------|----------------|
|
|
7
|
+
| `lib/agent.js` | Agent loop: iterates up to 10 times, streams LLM response, extracts tool calls, dispatches execution, accumulates results back into the message history |
|
|
8
|
+
| `lib/api.js` | HTTP client for both OpenAI-compatible inference (`chatStream`, `chatSync`) and dashboard REST calls (auth, models, chat history); manually parses `text/event-stream` |
|
|
9
|
+
| `lib/args.js` | CLI argument parser; maps flags (`-m`, `-f`, `--dry-run`, …) to an `opts` object and collects positional args |
|
|
10
|
+
| `lib/commands.js` | All top-level command handlers: `cmdChat`, `cmdCode`, `cmdEdit`, `cmdShell`, `cmdLogin`, `cmdLogout`, `cmdWhoAmI`, `cmdModels`, `cmdInit` |
|
|
11
|
+
| `lib/config.js` | Read/write `~/.semalt-ai/config.json`; `normalizeConfig` merges defaults, migrates legacy keys, validates types |
|
|
12
|
+
| `lib/constants.js` | `DEFAULT_CONFIG`, `DEFAULT_API_TIMEOUT_MS`, `CONFIG_PATH`, `PACKAGE_JSON` |
|
|
13
|
+
| `lib/context.js` | Loads file or directory trees into a prompt context string; caps directory walks at 50 files, single files at 10 000 chars |
|
|
14
|
+
| `lib/permissions.js` | Per-session approval tracking; interactive yes/always/no prompt before each tool action; `toggleAll` for `/approve` |
|
|
15
|
+
| `lib/prompts.js` | Returns the system prompt string that instructs the LLM which XML tool tags to use and how |
|
|
16
|
+
| `lib/tools.js` | Implements `agentExecShell` and `agentExecFile` (16 file/env/network actions) plus `extractToolCalls` regex parser |
|
|
17
|
+
| `lib/ui.js` | All terminal rendering: ANSI constants, `StreamRenderer` (markdown + tool-call display + inline diff), `readInteractiveInput`, `interactiveSelect`, `printBanner`, `printStatusBar` |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Agent Loop Flow
|
|
22
|
+
|
|
23
|
+
1. `runAgentLoop(messages, model)` is called with the current message array (max 10 iterations).
|
|
24
|
+
2. `chatStream(messages, { model })` sends the array to the LLM and streams tokens through `StreamRenderer` to the terminal.
|
|
25
|
+
3. The full assistant text is appended to `messages` as `{ role: 'assistant', content }`.
|
|
26
|
+
4. `extractToolCalls(reply)` scans the text for XML tool tags and fenced shell blocks; returns an ordered list of `[action, ...args]` tuples.
|
|
27
|
+
5. If no tool calls are found, the loop ends.
|
|
28
|
+
6. For each tool call, `permissionManager.askPermission(type, description)` prompts the user (yes / yes-always / no).
|
|
29
|
+
7. Approved shell calls go to `agentExecShell`; all other calls go to `agentExecFile`.
|
|
30
|
+
8. Results (or denial messages) are concatenated and pushed to `messages` as a `user` turn: `"Tool execution results:\n\n…\n\nContinue with the task."`.
|
|
31
|
+
9. If any call was denied, a warning is printed and the loop continues with partial results.
|
|
32
|
+
10. Go to step 2.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Tool Tags
|
|
37
|
+
|
|
38
|
+
All tags are parsed by `extractToolCalls` in `lib/tools.js` and displayed by `StreamRenderer` in `lib/ui.js`.
|
|
39
|
+
|
|
40
|
+
| Tag | Syntax | Action dispatched |
|
|
41
|
+
|-----|--------|-------------------|
|
|
42
|
+
| `exec` | `<exec>command</exec>` | `shell` |
|
|
43
|
+
| `shell` | `<shell>command</shell>` | `shell` (alias) |
|
|
44
|
+
| `run_command` | `<run_command>command</run_command>` | `shell` (alias) |
|
|
45
|
+
| `run` | `<run>command</run>` | `shell` (alias) |
|
|
46
|
+
| Fenced shell block | ` ```shell\ncommand\n``` ` | `shell` (one call per non-comment line) |
|
|
47
|
+
| `read_file` (content) | `<read_file>/path</read_file>` | `read` |
|
|
48
|
+
| `read_file` (attribute) | `<read_file path="/path"/>` | `read` |
|
|
49
|
+
| `write_file` | `<write_file path="/path">content</write_file>` | `write` |
|
|
50
|
+
| `append_file` | `<append_file path="/path">content</append_file>` | `append` |
|
|
51
|
+
| `list_dir` | `<list_dir>/path</list_dir>` | `list_dir` |
|
|
52
|
+
| `search_files` | `<search_files>pattern</search_files>` | `search_files` |
|
|
53
|
+
| `delete_file` | `<delete_file>/path</delete_file>` | `delete_file` |
|
|
54
|
+
| `make_dir` | `<make_dir>/path</make_dir>` | `make_dir` |
|
|
55
|
+
| `remove_dir` | `<remove_dir>/path</remove_dir>` | `remove_dir` |
|
|
56
|
+
| `get_env` | `<get_env>VAR_NAME</get_env>` | `get_env` |
|
|
57
|
+
| `set_env` | `<set_env name="VAR" value="val"/>` | `set_env` |
|
|
58
|
+
| `move_file` | `<move_file src="/src" dst="/dst"/>` | `move_file` |
|
|
59
|
+
| `copy_file` | `<copy_file src="/src" dst="/dst"/>` | `copy_file` |
|
|
60
|
+
| `edit_file` | `<edit_file path="/path" line="N">new line content</edit_file>` | `edit_file` |
|
|
61
|
+
| `search_in_file` | `<search_in_file path="/path">regex</search_in_file>` | `search_in_file` |
|
|
62
|
+
| `replace_in_file` | `<replace_in_file path="/path" search="old" replace="new"></replace_in_file>` | `replace_in_file` |
|
|
63
|
+
| `download` | `<download>https://url</download>` | `download` |
|
|
64
|
+
| `upload` | `<upload path="/path">base64content</upload>` | `upload` |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## DEFAULT_CONFIG Keys
|
|
69
|
+
|
|
70
|
+
Defined in `lib/constants.js` and merged/validated by `lib/config.js`.
|
|
71
|
+
|
|
72
|
+
| Key | Default | Description |
|
|
73
|
+
|-----|---------|-------------|
|
|
74
|
+
| `api_base` | `"http://127.0.0.1:8800"` | OpenAI-compatible inference base URL (normalized to include `/v1`) |
|
|
75
|
+
| `api_key` | `"any"` | API key sent as `Authorization: Bearer` to the inference endpoint |
|
|
76
|
+
| `dashboard_url` | `"https://cli.semalt.ai"` | Base URL for the web dashboard (auth, models, chat history) |
|
|
77
|
+
| `auth_token` | `""` | Bearer token written by `semalt login`, cleared by `logout` |
|
|
78
|
+
| `default_model` | `"default"` | Model identifier sent in chat completions payloads |
|
|
79
|
+
| `dashboard_model_id` | `null` | Integer PK of the active model in `available_models`; required for chat history sync |
|
|
80
|
+
| `temperature` | `0.7` | Sampling temperature passed to the LLM |
|
|
81
|
+
| `request_timeout_ms` | `900000` | HTTP request timeout for inference calls (ms) |
|
|
82
|
+
| `stream` | `true` | Whether to use streaming (`text/event-stream`) for inference |
|
|
83
|
+
| `models` | `[]` | Local model profile overrides (each: `api_base`, `api_key`, `model`) |
|
|
84
|
+
| `theme` | `"dark"` | Terminal color theme |
|
|
85
|
+
| `max_file_size_kb` | `512` | Maximum file size the agent will read (KB) |
|
|
86
|
+
| `command_timeout_ms` | `30000` | Timeout for shell command execution (ms) |
|
|
87
|
+
| `max_output_lines` | `50` | Maximum lines of tool output shown before truncation |
|
|
88
|
+
| `show_token_count` | `true` | Display token usage in the status line after each turn |
|
|
89
|
+
| `show_cost` | `false` | Display estimated cost alongside token count |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Planned Additions
|
|
94
|
+
|
|
95
|
+
| File | Intended Responsibility |
|
|
96
|
+
|------|------------------------|
|
|
97
|
+
| `lib/audit.js` | Persistent log of all tool calls executed per session (command, args, exit code, timestamp); used for replay and compliance review |
|
|
98
|
+
| `lib/storage.js` | Local SQLite or JSON-lines store for offline chat history, message deduplication, and cache of dashboard responses |
|
|
99
|
+
| `lib/metrics.js` | Collects per-session telemetry (token counts, latency, tool-call frequency) and exposes summary output for `/compact` and future analytics export |
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# semalt-code — CLI Agent
|
|
2
|
+
|
|
3
|
+
Node.js CLI tool that lets AI agents interact with code via an iterative tool-use loop. Zero external dependencies; uses only Node.js built-ins.
|
|
4
|
+
|
|
5
|
+
Published as `@semalt-ai/code`. Invokable as `semalt-code` or `semalt`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Directory Layout
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
semalt-code/
|
|
13
|
+
├── index.js # Entry point: arg parsing, module wiring, command dispatch
|
|
14
|
+
├── lib/
|
|
15
|
+
│ ├── api.js # HTTP client for dashboard auth + OpenAI-compatible inference
|
|
16
|
+
│ ├── agent.js # Agent loop: stream → extract tools → execute → repeat
|
|
17
|
+
│ ├── commands.js # All CLI command handlers (chat, code, edit, shell, login, …)
|
|
18
|
+
│ ├── tools.js # File and shell operation implementations
|
|
19
|
+
│ ├── prompts.js # System prompt for the LLM (tells it to use exec/read/write tags)
|
|
20
|
+
│ ├── ui.js # Barrel: re-exports everything from lib/ui/
|
|
21
|
+
│ ├── ui/
|
|
22
|
+
│ │ ├── ansi.js # ANSI escape constants, THEME, color codes, SPINNER_DEFS
|
|
23
|
+
│ │ ├── utils.js # getCols, getRows, stripAnsi, hr, boxLine, insertCharAt, …
|
|
24
|
+
│ │ ├── diff.js # renderDiff (LCS diff), renderMarkdown, _mdInline
|
|
25
|
+
│ │ ├── stream.js # StreamRenderer — live token-by-token terminal output
|
|
26
|
+
│ │ ├── legacy.js # StatusBar (cmdCode/cmdEdit), interactiveSelect, SelectMenu
|
|
27
|
+
│ │ ├── layout.js # LayoutManager — terminal geometry, resize events
|
|
28
|
+
│ │ ├── chat-history.js# ChatHistory — bubble rendering, scroll, streaming slots
|
|
29
|
+
│ │ ├── status-bar.js # FullStatusBar — animated TUI status line
|
|
30
|
+
│ │ ├── input-field.js # InputField, parseKeySequence, SLASH_CMDS
|
|
31
|
+
│ │ └── create-ui.js # createUI factory + non-TTY no-op fallback
|
|
32
|
+
│ ├── context.js # Loads file/directory content into the prompt
|
|
33
|
+
│ ├── config.js # Read/write ~/.semalt-ai/config.json
|
|
34
|
+
│ ├── permissions.js # Per-session approval tracking for tool calls
|
|
35
|
+
│ ├── args.js # CLI argument parser
|
|
36
|
+
│ ├── constants.js # CONFIG_PATH, DEFAULT_CONFIG, DEFAULT_API_TIMEOUT_MS
|
|
37
|
+
│ ├── audit.js # Append-only audit log for all tool executions
|
|
38
|
+
│ ├── storage.js # Local session persistence and resume
|
|
39
|
+
│ └── metrics.js # Token counting, cost estimation, latency tracking
|
|
40
|
+
├── package.json # name: @semalt-ai/code, version: 1.8.0, bin: semalt / semalt-code
|
|
41
|
+
└── README.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Tech Stack
|
|
47
|
+
|
|
48
|
+
| Component | Technology |
|
|
49
|
+
|-----------|-----------|
|
|
50
|
+
| Runtime | Node.js ≥ 16, CommonJS (`require`) |
|
|
51
|
+
| HTTP | Built-in `http`/`https` modules |
|
|
52
|
+
| Shell exec | `child_process.spawnSync` |
|
|
53
|
+
| File I/O | `fs` module |
|
|
54
|
+
| Terminal UI | Raw ANSI escape codes (no chalk, no readline wrappers) |
|
|
55
|
+
| Config | JSON at `~/.semalt-ai/config.json` |
|
|
56
|
+
| LLM protocol | OpenAI-compatible streaming (`text/event-stream`) |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## CLI Commands
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
semalt-code # interactive chat (default)
|
|
64
|
+
semalt-code chat # interactive chat (explicit)
|
|
65
|
+
semalt-code code <prompt> # one-shot task with optional file context
|
|
66
|
+
semalt-code edit <file> <instruction> # targeted file edit
|
|
67
|
+
semalt-code shell <command> # run shell, optionally ask LLM to analyze output
|
|
68
|
+
semalt-code login # browser-based device auth against dashboard
|
|
69
|
+
semalt-code logout # clear stored auth_token
|
|
70
|
+
semalt-code whoami # show authenticated user
|
|
71
|
+
semalt-code models # interactive model selector (fetches from dashboard)
|
|
72
|
+
semalt-code init [options] # create/update ~/.semalt-ai/config.json
|
|
73
|
+
semalt-code audit # print last 50 audit log entries
|
|
74
|
+
semalt-code config [set <key> <val>] # show or update config keys
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Common Flags
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
-m, --model <name> override model for this invocation
|
|
81
|
+
-r, --resume <chat-id> resume a dashboard chat by ID
|
|
82
|
+
-f, --file <path> load file or directory as context
|
|
83
|
+
-a, --analyze have LLM analyze shell output (used with `shell`)
|
|
84
|
+
--dry-run preview file edits without writing
|
|
85
|
+
--api-base <url> LLM API base URL (overrides config)
|
|
86
|
+
--api-key <key> API key (overrides config)
|
|
87
|
+
--dashboard-url <url> dashboard base URL (overrides config)
|
|
88
|
+
--default-model <name> set default model in config
|
|
89
|
+
--show-think display model reasoning (thinking) content
|
|
90
|
+
--debug print raw AI response (stderr) each iteration
|
|
91
|
+
--allow-fs auto-approve all filesystem operations
|
|
92
|
+
--allow-exec auto-approve shell command execution
|
|
93
|
+
--allow-net auto-approve network operations
|
|
94
|
+
--allow-all auto-approve everything (use carefully)
|
|
95
|
+
--readonly block all write operations
|
|
96
|
+
--new skip session resume prompt
|
|
97
|
+
-v, --version print version
|
|
98
|
+
-h, --help print help
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### In-Chat Slash Commands
|
|
102
|
+
|
|
103
|
+
| Command | Effect |
|
|
104
|
+
|---------|--------|
|
|
105
|
+
| `/help` | List slash commands |
|
|
106
|
+
| `/file <path>` | Attach file or directory to context |
|
|
107
|
+
| `/history` | Browse and load a local saved session |
|
|
108
|
+
| `/chats` | Browse and resume a saved chat from the dashboard |
|
|
109
|
+
| `/new` | Start a fresh conversation (detach from current saved chat) |
|
|
110
|
+
| `/model [name]` | Show or switch model |
|
|
111
|
+
| `/models` | Interactive model picker from dashboard |
|
|
112
|
+
| `/shell <cmd>` or `!<cmd>` | Execute shell command |
|
|
113
|
+
| `/compact` | Show token usage estimate and session metrics |
|
|
114
|
+
| `/clear` | Reset conversation history |
|
|
115
|
+
| `/approve` | Toggle auto-approval of tool calls |
|
|
116
|
+
| `/config` | Print current config |
|
|
117
|
+
| `/login` | Start device auth flow |
|
|
118
|
+
| `/whoami` | Show current user |
|
|
119
|
+
| `/logout` | Clear auth token |
|
|
120
|
+
| `exit` / `quit` | Exit |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Agent Loop (`lib/agent.js`)
|
|
125
|
+
|
|
126
|
+
Maximum 10 iterations per user turn.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
1. Send messages[] to LLM via chatStream()
|
|
130
|
+
2. Stream response tokens to terminal (StreamRenderer)
|
|
131
|
+
3. After full response: extract tool-call tags from text
|
|
132
|
+
4. If no tool tags → done
|
|
133
|
+
5. For each tag: request user permission (once / always / no)
|
|
134
|
+
6. Execute approved operations via ToolExecutor (wrapped in try/catch)
|
|
135
|
+
7. Append tool results to messages[]
|
|
136
|
+
8. Goto 1
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Each tool dispatch is wrapped in try/catch; errors print a warning and continue to the next tag rather than aborting the loop.
|
|
140
|
+
|
|
141
|
+
### Tool Tags (parsed from LLM text)
|
|
142
|
+
|
|
143
|
+
```xml
|
|
144
|
+
<exec>shell command here</exec>
|
|
145
|
+
<shell>shell command here</shell>
|
|
146
|
+
<read_file>/absolute/or/relative/path</read_file>
|
|
147
|
+
<read_file path="/path/to/file"/>
|
|
148
|
+
<write_file path="/path/to/file">file content here</write_file>
|
|
149
|
+
<create_file path="/path/to/file">file content here</create_file>
|
|
150
|
+
<append_file path="/path/to/file">content to append</append_file>
|
|
151
|
+
<list_dir>/path/to/dir</list_dir>
|
|
152
|
+
<search_files pattern="*.ts" dir="src"/>
|
|
153
|
+
<delete_file>/path/to/file</delete_file>
|
|
154
|
+
<make_dir>/path/to/dir</make_dir>
|
|
155
|
+
<remove_dir>/path/to/dir</remove_dir>
|
|
156
|
+
<get_env>ENV_VAR_NAME</get_env>
|
|
157
|
+
<set_env name="VAR" value="value"/>
|
|
158
|
+
<move_file src="/old/path" dst="/new/path"/>
|
|
159
|
+
<copy_file src="/src/path" dst="/dst/path"/>
|
|
160
|
+
<edit_file path="/file" line="42">replacement line content</edit_file>
|
|
161
|
+
<search_in_file path="/file">regex pattern</search_in_file>
|
|
162
|
+
<replace_in_file path="/file" search="old" replace="new"></replace_in_file>
|
|
163
|
+
<download>https://example.com/file.zip</download>
|
|
164
|
+
<upload path="/local/path">base64encodedcontent</upload>
|
|
165
|
+
<file_stat>/path/to/file</file_stat>
|
|
166
|
+
<http_get url="https://example.com/api"/>
|
|
167
|
+
<ask_user question="What is your preferred language?"/>
|
|
168
|
+
<store_memory key="project_lang">TypeScript</store_memory>
|
|
169
|
+
<recall_memory key="project_lang"/>
|
|
170
|
+
<list_memories/>
|
|
171
|
+
<system_info/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The system prompt (`lib/prompts.js`) instructs the LLM to use exactly these tags. Do not change tag names without updating both `prompts.js` and the parser in `agent.js`.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Tool Operations (`lib/tools.js`)
|
|
179
|
+
|
|
180
|
+
All operations request permission before execution unless auto-approved.
|
|
181
|
+
Output truncated to `config.max_output_lines` (default 20) to avoid filling context.
|
|
182
|
+
|
|
183
|
+
| Action | Description |
|
|
184
|
+
|--------|-------------|
|
|
185
|
+
| `read` | Read file content |
|
|
186
|
+
| `write` | Write file (creates parent dirs) |
|
|
187
|
+
| `append` | Append to file |
|
|
188
|
+
| `list_dir` | List directory contents |
|
|
189
|
+
| `delete_file` | Delete file |
|
|
190
|
+
| `make_dir` | Create directory (recursive) |
|
|
191
|
+
| `remove_dir` | Remove directory (recursive) |
|
|
192
|
+
| `move_file` | Move/rename file |
|
|
193
|
+
| `copy_file` | Copy file |
|
|
194
|
+
| `search_files` | Find files matching glob pattern |
|
|
195
|
+
| `search_in_file` | Regex search within file |
|
|
196
|
+
| `replace_in_file` | Replace text in file (regex, optional flags) |
|
|
197
|
+
| `edit_file` | Replace a specific line number in a file |
|
|
198
|
+
| `get_env` / `set_env` | Read/write environment variables |
|
|
199
|
+
| `download` | HTTP GET → save to file |
|
|
200
|
+
| `upload` | Write base64-encoded content to file |
|
|
201
|
+
| `file_stat` | Stat a file (size, mtime, type, mode) |
|
|
202
|
+
| `http_get` | HTTP GET → return body (truncated to max_output_lines) |
|
|
203
|
+
| `ask_user` | Prompt user for input; auto-answers 'y' in non-TTY mode |
|
|
204
|
+
| `store_memory` | Persist a key/value pair to `~/.semalt-ai/memory.json` |
|
|
205
|
+
| `recall_memory` | Read a key from `~/.semalt-ai/memory.json` |
|
|
206
|
+
| `list_memories` | List all stored memory keys |
|
|
207
|
+
| `system_info` | Return platform, arch, hostname, memory, Node version, cwd |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Audit Log (`lib/audit.js`)
|
|
212
|
+
|
|
213
|
+
Every tool execution is appended to `~/.semalt-ai/audit.log` as NDJSON:
|
|
214
|
+
```json
|
|
215
|
+
{"ts":"2026-01-01T00:00:00.000Z","tag":"exec","input":"{\"command\":\"ls\"}","approved":true,"result":"ok"}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
View the last 50 entries with `semalt-code audit`.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Session Storage (`lib/storage.js`)
|
|
223
|
+
|
|
224
|
+
Local chat sessions are saved to `~/.semalt-ai/sessions/` as JSON files named `<timestamp>-<id>.json`. The `chat` command offers to resume the most recent session (< 24 h old) on startup unless `--new` or `--resume` is passed. Use `/history` in-chat to browse and load any saved session.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Metrics (`lib/metrics.js`)
|
|
229
|
+
|
|
230
|
+
`Metrics` is instantiated per `runAgentLoop` call and tracks per-turn token usage, latency, and total session duration. A summary box is printed on exit (SIGINT or natural quit) and after `cmdCode` runs. Use `/compact` in-chat to see the live summary.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## API Client (`lib/api.js`)
|
|
235
|
+
|
|
236
|
+
Handles two distinct concerns:
|
|
237
|
+
|
|
238
|
+
**Inference** (OpenAI-compatible):
|
|
239
|
+
- `chatStream(messages, model, opts)` → streams tokens, calls `onToken`, returns `{ content, usage }`
|
|
240
|
+
- URL: `config.api_base` normalized to include `/v1` if missing
|
|
241
|
+
- Supports `reasoning_content` field for extended-thinking models
|
|
242
|
+
|
|
243
|
+
**Dashboard** (cli.semalt.ai backend):
|
|
244
|
+
- `requestCliLogin()` → `POST /api/auth/cli/request`
|
|
245
|
+
- `getCliLoginStatus(id, token)` → `POST /api/auth/cli/status`
|
|
246
|
+
- `dashboardWhoAmI()` → `GET /api/auth/me`
|
|
247
|
+
- `dashboardLogout()` → `POST /api/auth/logout`
|
|
248
|
+
- `dashboardListModels()` → `GET /api/models`
|
|
249
|
+
- `dashboardGetModelForCli(id)` → `GET /api/models/{id}/cli`
|
|
250
|
+
- `dashboardCreateChat(title, modelDbId)` → `POST /api/chats`
|
|
251
|
+
- `dashboardListChats()` → `GET /api/chats`
|
|
252
|
+
- `dashboardGetChat(id)` → `GET /api/chats/{id}`
|
|
253
|
+
- `dashboardSaveMessages(chatId, messages)` → `POST /api/chats/{id}/messages/batch`
|
|
254
|
+
|
|
255
|
+
All dashboard calls send `Authorization: Bearer <auth_token>` from config.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Config File (`~/.semalt-ai/config.json`)
|
|
260
|
+
|
|
261
|
+
Managed by `lib/config.js`. Normalized on every load. The config directory is created automatically if it does not exist.
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"api_base": "http://127.0.0.1:8800",
|
|
266
|
+
"api_key": "any",
|
|
267
|
+
"dashboard_url": "https://cli.semalt.ai",
|
|
268
|
+
"auth_token": "",
|
|
269
|
+
"default_model": "default",
|
|
270
|
+
"dashboard_model_id": null,
|
|
271
|
+
"temperature": 0.7,
|
|
272
|
+
"request_timeout_ms": 900000,
|
|
273
|
+
"stream": true,
|
|
274
|
+
"theme": "dark",
|
|
275
|
+
"max_file_size_kb": 512,
|
|
276
|
+
"command_timeout_ms": 30000,
|
|
277
|
+
"max_output_lines": 50,
|
|
278
|
+
"show_token_count": true,
|
|
279
|
+
"show_cost": false,
|
|
280
|
+
"context_length": null,
|
|
281
|
+
"models": [
|
|
282
|
+
{
|
|
283
|
+
"name": "local-llama",
|
|
284
|
+
"api_base": "http://127.0.0.1:11434",
|
|
285
|
+
"api_key": "any",
|
|
286
|
+
"model": "llama3",
|
|
287
|
+
"context_length": 8192
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- `api_base` is normalized to always include `/v1`.
|
|
294
|
+
- Legacy key `semalt_base_url` is migrated to `api_base` on load.
|
|
295
|
+
- `auth_token` is written by `semalt-code login` and cleared by `logout`.
|
|
296
|
+
- `dashboard_model_id` is the integer PK of the active model in `available_models`; written when a model is selected via `/models`. Required for chat history sync — if null, history sync is silently skipped.
|
|
297
|
+
- `max_file_size_kb` caps how large a file may be before read is refused (default 512 KB).
|
|
298
|
+
- `command_timeout_ms` caps shell command execution time (default 30 s).
|
|
299
|
+
- `max_output_lines` caps shell and HTTP response lines returned to the agent (default 50).
|
|
300
|
+
- `show_token_count` controls whether token count is shown in the status bar.
|
|
301
|
+
- `show_cost` reserved for future cost-display feature.
|
|
302
|
+
- `context_length` / `models[].context_length` — token limit used for context-usage bar and warnings.
|
|
303
|
+
- Local `models[]` entries override dashboard models when selected.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Key Patterns & Invariants
|
|
308
|
+
|
|
309
|
+
- **No dependencies**: keep it that way. Any new feature must use Node.js built-ins only.
|
|
310
|
+
- **CommonJS**: all files use `require()`/`module.exports`. Do not use ES `import`/`export`.
|
|
311
|
+
- **Streaming**: `api.js` manually parses `text/event-stream`. The parser in `chatStream()` handles partial JSON lines — be careful editing it.
|
|
312
|
+
- **Permissions are per-session**: `PermissionManager` resets on each CLI invocation. Approvals never persist to disk. In non-TTY mode all tool calls are auto-approved with a warning.
|
|
313
|
+
- **Token counting is approximate**: `estimateTokens()` divides char count by 4. It is used only for the `/compact` display — do not rely on it for hard limits.
|
|
314
|
+
- **Tool output is truncated**: `tools.js` caps output at `max_output_lines` (default 50). Configurable via config.
|
|
315
|
+
- **Max 10 agent iterations**: hard-coded in `agent.js`. Prevents runaway loops.
|
|
316
|
+
- **Malformed tags are skipped**: each tool dispatch in the agent loop is wrapped in try/catch; errors emit a warning line and continue to the next tool call.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Development & Publishing
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
# Run locally
|
|
324
|
+
node index.js chat
|
|
325
|
+
|
|
326
|
+
# Symlink for global use during dev
|
|
327
|
+
npm link
|
|
328
|
+
|
|
329
|
+
# Publish to npm
|
|
330
|
+
npm publish --access public
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Version is in `package.json`. Bump it with every published change.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Keeping This File Up-to-Date
|
|
338
|
+
|
|
339
|
+
Update this file when:
|
|
340
|
+
- A new CLI command or slash command is added (update the commands tables).
|
|
341
|
+
- A new tool action is added to `tools.js` (update the Tool Operations table).
|
|
342
|
+
- The agent loop behavior changes (max iterations, tag format, approval flow).
|
|
343
|
+
- A new `lib/` module is added.
|
|
344
|
+
- The config schema changes (new keys, renamed keys, migration logic).
|
|
345
|
+
- A new dashboard API call is added to `api.js`.
|
|
346
|
+
- The system prompt in `prompts.js` changes in a way that affects tool-tag syntax.
|
|
347
|
+
- The Node.js version requirement changes.
|
|
348
|
+
|
|
349
|
+
When renaming or removing a tool tag, update **both** `prompts.js` and `agent.js` atomically and note it here.
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ semalt-code
|
|
|
49
49
|
Create the CLI config:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
semalt-code init --api-base http://127.0.0.1:8800 --api-key any --default-model default
|
|
52
|
+
semalt-code init --api-base http://127.0.0.1:8800 --api-key any --dashboard-url https://cli.semalt.ai --default-model default
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
This writes configuration to:
|
|
@@ -64,6 +64,8 @@ Example config:
|
|
|
64
64
|
{
|
|
65
65
|
"api_base": "http://127.0.0.1:8800",
|
|
66
66
|
"api_key": "any",
|
|
67
|
+
"dashboard_url": "https://cli.semalt.ai",
|
|
68
|
+
"auth_token": "",
|
|
67
69
|
"default_model": "default",
|
|
68
70
|
"temperature": 0.7,
|
|
69
71
|
"request_timeout_ms": 900000,
|
|
@@ -94,8 +96,17 @@ semalt-code [command] [options]
|
|
|
94
96
|
- `semalt-code shell <command>`
|
|
95
97
|
Runs a shell command with approval prompts.
|
|
96
98
|
|
|
99
|
+
- `semalt-code login`
|
|
100
|
+
Starts browser-based login and stores the confirmed CLI token in `~/.semalt-ai/config.json`.
|
|
101
|
+
|
|
102
|
+
- `semalt-code whoami`
|
|
103
|
+
Shows the current user associated with the saved CLI auth token.
|
|
104
|
+
|
|
105
|
+
- `semalt-code logout`
|
|
106
|
+
Logs out the current CLI user and clears the saved local auth token.
|
|
107
|
+
|
|
97
108
|
- `semalt-code models`
|
|
98
|
-
Lists
|
|
109
|
+
Lists your models from the dashboard and lets you choose the current one for the CLI.
|
|
99
110
|
|
|
100
111
|
- `semalt-code models add`
|
|
101
112
|
Opens an interactive flow to add an API base URL, API key, and model ID as a reusable model profile.
|
|
@@ -137,7 +148,10 @@ Available interactive commands:
|
|
|
137
148
|
|
|
138
149
|
- `/help`
|
|
139
150
|
- `/file <path>`
|
|
151
|
+
- `/whoami`
|
|
152
|
+
- `/logout`
|
|
140
153
|
- `/model`
|
|
154
|
+
- `/login`
|
|
141
155
|
- `/model <name>`
|
|
142
156
|
- `/models`
|
|
143
157
|
- `/clear`
|
package/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
4
8
|
const { PACKAGE_JSON } = require('./lib/constants');
|
|
5
|
-
const { loadConfig, saveConfig } = require('./lib/config');
|
|
9
|
+
const { loadConfig, saveConfig, configSet, configShow } = require('./lib/config');
|
|
6
10
|
const ui = require('./lib/ui');
|
|
7
11
|
const { createPermissionManager } = require('./lib/permissions');
|
|
8
12
|
const { createToolExecutor, extractToolCalls } = require('./lib/tools');
|
|
@@ -12,6 +16,7 @@ const { createAgentRunner } = require('./lib/agent');
|
|
|
12
16
|
const { createCommands } = require('./lib/commands');
|
|
13
17
|
const { parseArgs } = require('./lib/args');
|
|
14
18
|
const { CONFIG_PATH } = require('./lib/constants');
|
|
19
|
+
const { AUDIT_LOG } = require('./lib/audit');
|
|
15
20
|
|
|
16
21
|
let config = loadConfig();
|
|
17
22
|
|
|
@@ -24,8 +29,20 @@ function setConfig(nextConfig) {
|
|
|
24
29
|
saveConfig(config);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
const
|
|
32
|
+
// Pre-scan argv for permission tier flags before creating PermissionManager
|
|
33
|
+
const _argv = process.argv.slice(2);
|
|
34
|
+
const _allowedTiers = [];
|
|
35
|
+
if (_argv.includes('--allow-all')) {
|
|
36
|
+
_allowedTiers.push('fs', 'exec', 'net', 'sys');
|
|
37
|
+
} else {
|
|
38
|
+
if (_argv.includes('--allow-fs')) _allowedTiers.push('fs');
|
|
39
|
+
if (_argv.includes('--allow-exec')) _allowedTiers.push('exec');
|
|
40
|
+
if (_argv.includes('--allow-net')) _allowedTiers.push('net');
|
|
41
|
+
}
|
|
42
|
+
const _readonly = _argv.includes('--readonly');
|
|
43
|
+
|
|
44
|
+
const permissionManager = createPermissionManager(ui, { allowedTiers: _allowedTiers, readonly: _readonly });
|
|
45
|
+
const { agentExecShell, agentExecFile } = createToolExecutor(permissionManager, ui, getConfig);
|
|
29
46
|
const apiClient = createApiClient({
|
|
30
47
|
getConfig,
|
|
31
48
|
saveConfig: (nextConfig) => {
|
|
@@ -74,18 +91,30 @@ Commands:
|
|
|
74
91
|
code <prompt> Generate code from a prompt
|
|
75
92
|
edit <file> <instruction> Edit a file with AI
|
|
76
93
|
shell <command> Run and optionally analyze a shell command
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
login Authorize CLI via browser
|
|
95
|
+
whoami Show current authorized user
|
|
96
|
+
logout Clear current CLI login
|
|
97
|
+
models Choose a model
|
|
79
98
|
init Initialize config
|
|
80
99
|
|
|
81
100
|
Options:
|
|
82
101
|
-m, --model <name> Model name
|
|
102
|
+
-r, --resume <chat-id> Resume a saved chat (chat command)
|
|
83
103
|
-f, --file <path> Load file into context (code command)
|
|
84
104
|
-a, --analyze Analyze output with AI (shell command)
|
|
85
105
|
--dry-run Don't save changes (edit command)
|
|
86
106
|
--api-base <url> API base URL (init)
|
|
87
107
|
--api-key <key> API key (init)
|
|
108
|
+
--dashboard-url <url> Dashboard URL (init)
|
|
88
109
|
--default-model <name> Default model (init)
|
|
110
|
+
--show-think Display model reasoning (thinking) content
|
|
111
|
+
--debug Print messages sent to agent + raw AI response (stderr) each iteration
|
|
112
|
+
--allow-fs Auto-approve all filesystem operations
|
|
113
|
+
--allow-exec Auto-approve shell command execution
|
|
114
|
+
--allow-net Auto-approve network operations
|
|
115
|
+
--allow-all Auto-approve everything (use carefully)
|
|
116
|
+
--readonly Block all write operations
|
|
117
|
+
--new Skip session resume prompt
|
|
89
118
|
-v, --version Show CLI version
|
|
90
119
|
|
|
91
120
|
Config: ${CONFIG_PATH}
|
|
@@ -110,12 +139,55 @@ Config: ${CONFIG_PATH}
|
|
|
110
139
|
} else if (command === 'shell') {
|
|
111
140
|
const { opts, positional } = parseArgs(rawArgs.slice(1));
|
|
112
141
|
await commands.cmdShell(opts, positional);
|
|
142
|
+
} else if (command === 'login') {
|
|
143
|
+
await commands.cmdLogin();
|
|
144
|
+
} else if (command === 'whoami') {
|
|
145
|
+
await commands.cmdWhoAmI();
|
|
146
|
+
} else if (command === 'logout') {
|
|
147
|
+
await commands.cmdLogout();
|
|
113
148
|
} else if (command === 'models') {
|
|
114
|
-
|
|
115
|
-
else await commands.cmdModels();
|
|
149
|
+
await commands.cmdModels();
|
|
116
150
|
} else if (command === 'init') {
|
|
117
151
|
const { opts } = parseArgs(rawArgs.slice(1));
|
|
118
152
|
commands.cmdInit(opts);
|
|
153
|
+
} else if (command === 'audit') {
|
|
154
|
+
try {
|
|
155
|
+
const content = fs.readFileSync(AUDIT_LOG, 'utf8');
|
|
156
|
+
const lines = content.trim().split('\n').filter((l) => l.trim());
|
|
157
|
+
const last50 = lines.slice(-50);
|
|
158
|
+
for (const line of last50) {
|
|
159
|
+
try {
|
|
160
|
+
const entry = JSON.parse(line);
|
|
161
|
+
const icon = entry.approved ? `${ui.FG_GREEN}✓${ui.RST}` : `${ui.FG_RED}✗${ui.RST}`;
|
|
162
|
+
console.log(`${icon} ${line}`);
|
|
163
|
+
} catch {
|
|
164
|
+
console.log(line);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
console.log('No audit log found.');
|
|
169
|
+
}
|
|
170
|
+
} else if (command === 'config') {
|
|
171
|
+
const sub = rawArgs[1];
|
|
172
|
+
if (sub === 'set') {
|
|
173
|
+
const key = rawArgs[2];
|
|
174
|
+
const value = rawArgs[3];
|
|
175
|
+
if (!key || value === undefined) {
|
|
176
|
+
process.stderr.write(`Usage: semalt-code config set <key> <value>\n`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(value);
|
|
182
|
+
} catch {
|
|
183
|
+
parsed = value;
|
|
184
|
+
}
|
|
185
|
+
configSet(key, parsed);
|
|
186
|
+
console.log(`Set ${key} = ${JSON.stringify(parsed)}`);
|
|
187
|
+
} else {
|
|
188
|
+
// default: "show" or bare "config"
|
|
189
|
+
console.log(configShow());
|
|
190
|
+
}
|
|
119
191
|
} else {
|
|
120
192
|
const { opts } = parseArgs(rawArgs);
|
|
121
193
|
await commands.cmdChat(opts);
|