@robota-sdk/agent-cli 3.0.0-beta.8 → 3.0.0-beta.81
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/LICENSE +661 -21
- package/README.md +1105 -116
- package/bin/robota.cjs +39 -0
- package/dist/node/bin.js +7 -9
- package/dist/node/bounded-output-DpjpS98g-DFgAppgl.js +2 -0
- package/dist/node/bounded-output-DpjpS98g-DFgAppgl.js.map +1 -0
- package/dist/node/bounded-output-DpjpS98g-DoRuRGhV.js +2 -0
- package/dist/node/bounded-output-DpjpS98g-yPybHprY.cjs +1 -0
- package/dist/node/cli-core-Bah38oug.js +2795 -0
- package/dist/node/command-executor-DV-H2oD6-BfKoGoFw.js +2 -0
- package/dist/node/command-executor-DV-H2oD6-OKy8pwYH.js +2 -0
- package/dist/node/command-executor-DV-H2oD6-OKy8pwYH.js.map +1 -0
- package/dist/node/command-executor-DV-H2oD6-iQAXtTLY.cjs +1 -0
- package/dist/node/headless.js +4 -0
- package/dist/node/http-executor-cWzI2svn-C7F17ZGz.cjs +1 -0
- package/dist/node/http-executor-cWzI2svn-TG_TMYKh.js +2 -0
- package/dist/node/http-executor-cWzI2svn-TG_TMYKh.js.map +1 -0
- package/dist/node/http-executor-cWzI2svn-Ype0naht.js +2 -0
- package/dist/node/index.cjs +2782 -1345
- package/dist/node/index.d.cts +3408 -36
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +3408 -36
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2794 -13
- package/dist/node/index.js.map +1 -0
- package/dist/node/node-2-nrieFn.js +2 -0
- package/dist/node/node-2-nrieFn.js.map +1 -0
- package/dist/node/node-C-hnFnOP.js +67 -0
- package/dist/node/node-CFTax5Wl.cjs +1 -0
- package/dist/node/node-CJnrbkAS.js +14 -0
- package/dist/node/node-CJnrbkAS.js.map +1 -0
- package/dist/node/node-ChG9Tg1I.js +2 -0
- package/dist/node/node-DDl02brx.cjs +66 -0
- package/dist/node/node-DIRGC_1P.js +67 -0
- package/dist/node/node-DIRGC_1P.js.map +1 -0
- package/dist/node/node-OBnZskvR.cjs +1 -0
- package/dist/node/node-bJ0lEuNF.js +2 -0
- package/dist/node/node-bJ0lEuNF.js.map +1 -0
- package/dist/node/node-tf433Aey.cjs +13 -0
- package/dist/node/node-uwhEt6sT.js +14 -0
- package/dist/node/rolldown-runtime-C9xYM0wc.js +1 -0
- package/dist/node/rolldown-runtime-DK3Fl9T5-BIInC43l.cjs +1 -0
- package/dist/node/rolldown-runtime-DK3Fl9T5-BeX3Bzvn.js +2 -0
- package/dist/node/rolldown-runtime-DK3Fl9T5-DK3Fl9T5.js +2 -0
- package/dist/node/rolldown-runtime-DK3Fl9T5-DK3Fl9T5.js.map +1 -0
- package/dist/node/rolldown-runtime-rs7quPpn.js +2 -0
- package/dist/node/verdict-decoder-Dk_w2NG9-BKWMtxp8.js +2 -0
- package/dist/node/verdict-decoder-Dk_w2NG9-Bka7Cldj.js +2 -0
- package/dist/node/verdict-decoder-Dk_w2NG9-Bka7Cldj.js.map +1 -0
- package/dist/node/verdict-decoder-Dk_w2NG9-DNTeVdQ8.cjs +1 -0
- package/dist/web/assets/index-8_Slbxe7.css +1 -0
- package/dist/web/assets/index-Cgu5C1kn.js +77 -0
- package/dist/web/index.html +13 -0
- package/package.json +159 -32
- package/dist/node/bin.cjs +0 -1339
- package/dist/node/bin.d.cts +0 -1
- package/dist/node/bin.d.ts +0 -1
- package/dist/node/chunk-ICKV3U6G.js +0 -1318
package/README.md
CHANGED
|
@@ -1,17 +1,86 @@
|
|
|
1
|
+
**Language:** [English](README.md) | [한국어](docs/README-KO.md)
|
|
2
|
+
|
|
3
|
+
> **Beta software** — currently `3.0.0-beta`. APIs and behavior may change before stable release.
|
|
4
|
+
> Please [report issues](https://github.com/woojubb/robota/issues) to help us improve.
|
|
5
|
+
|
|
1
6
|
# @robota-sdk/agent-cli
|
|
2
7
|
|
|
3
|
-
AI coding assistant CLI built on Robota SDK. Loads AGENTS.md/CLAUDE.md for project context and provides tool-calling REPL with Claude Code-compatible permission modes.
|
|
8
|
+
AI coding assistant CLI built on Robota SDK. Loads AGENTS.md/CLAUDE.md for project context and provides a tool-calling REPL with Claude Code-compatible permission modes.
|
|
4
9
|
|
|
5
|
-
##
|
|
10
|
+
## Why Robota?
|
|
11
|
+
|
|
12
|
+
| | Robota | Claude Code | Aider |
|
|
13
|
+
| ------------------------------------------------- | :----: | :---------: | :---: |
|
|
14
|
+
| Multi-provider (Anthropic, OpenAI, Gemini, Qwen…) | ✅ | ❌ | ✅ |
|
|
15
|
+
| Embed SDK in your own app | ✅ | ❌ | ❌ |
|
|
16
|
+
| Local models (LM Studio, Ollama via OpenAI API) | ✅ | ❌ | ✅ |
|
|
17
|
+
| Open source (AGPL-3.0) | ✅ | partial | ✅ |
|
|
18
|
+
| Claude Code config compatible (CLAUDE.md, modes) | ✅ | — | ❌ |
|
|
19
|
+
|
|
20
|
+
## Embed in Your App
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createAgentRuntime } from '@robota-sdk/agent-framework';
|
|
24
|
+
import { createAnthropicProvider } from '@robota-sdk/agent-provider-anthropic';
|
|
25
|
+
|
|
26
|
+
const runtime = createAgentRuntime({
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
provider: createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }),
|
|
29
|
+
// projectAccess: hostWorkspaceDecision,
|
|
30
|
+
});
|
|
31
|
+
const session = runtime.createSession({ permissionMode: 'bypassPermissions' });
|
|
32
|
+
|
|
33
|
+
// submit() is event-driven and resolves to void — drive output via listeners
|
|
34
|
+
session.on('text_delta', (delta) => process.stdout.write(delta));
|
|
35
|
+
session.on('complete', (result) => console.log(result.response));
|
|
36
|
+
|
|
37
|
+
await session.submit('Explain this codebase');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A bare `cwd` is provenance, not project authority. Without a host-issued
|
|
41
|
+
`TWorkspaceProjectAccess` decision the runtime is observably Restricted and does not load project
|
|
42
|
+
context, settings, memory, sessions, or logs. Embedders establish that decision through the
|
|
43
|
+
framework `WorkspaceTrustService`; a path, boolean, or generic filesystem cannot substitute for it.
|
|
44
|
+
Trusted composition is rejected when `cwd` resolves outside that authority's frozen workspace root.
|
|
45
|
+
|
|
46
|
+
## Prerequisites
|
|
47
|
+
|
|
48
|
+
Node.js **22 or higher** is required. The TUI renderer ([ink 7.x](https://github.com/vadimdemedes/ink)) requires Node.js 22+.
|
|
6
49
|
|
|
7
50
|
```bash
|
|
8
|
-
#
|
|
9
|
-
|
|
51
|
+
node --version # Must output v22.x.x or higher
|
|
52
|
+
```
|
|
10
53
|
|
|
11
|
-
|
|
54
|
+
If your version is below 22, upgrade using one of:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# nvm
|
|
58
|
+
nvm install 22 && nvm use 22
|
|
59
|
+
|
|
60
|
+
# Volta
|
|
61
|
+
volta install node@22
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Demo
|
|
65
|
+
|
|
66
|
+

|
|
67
|
+
|
|
68
|
+
Recorded from the real CLI with `pnpm --filter @robota-sdk/agent-cli demo:record`. The tools run for
|
|
69
|
+
real; the model turns are replayed from a recorded session log so the recording needs no API key —
|
|
70
|
+
see [docs/DEMO-SCRIPT.md](./docs/DEMO-SCRIPT.md).
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Try it now — no install needed
|
|
12
76
|
npx @robota-sdk/agent-cli
|
|
77
|
+
|
|
78
|
+
# Install globally for persistent use
|
|
79
|
+
npm install -g @robota-sdk/agent-cli
|
|
13
80
|
```
|
|
14
81
|
|
|
82
|
+
> **macOS users**: Korean/CJK IME input may crash macOS Terminal.app. Use **[iTerm2](https://iterm2.com/)** instead. This is a known Ink + Terminal.app issue shared with Claude Code.
|
|
83
|
+
|
|
15
84
|
After installing globally, the `robota` command is available system-wide:
|
|
16
85
|
|
|
17
86
|
```bash
|
|
@@ -22,9 +91,14 @@ robota -p "List all files" # Print mode (one-shot, exit after response)
|
|
|
22
91
|
|
|
23
92
|
### Environment Variables
|
|
24
93
|
|
|
25
|
-
| Variable | Description
|
|
26
|
-
| ------------------- |
|
|
27
|
-
| `ANTHROPIC_API_KEY` | Anthropic API key
|
|
94
|
+
| Variable | Description | Provider |
|
|
95
|
+
| ------------------- | -------------------------------------------------------- | --------- |
|
|
96
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key | Anthropic |
|
|
97
|
+
| `OPENAI_API_KEY` | OpenAI API key | OpenAI |
|
|
98
|
+
| `GEMINI_API_KEY` | Google Gemini API key | Gemini |
|
|
99
|
+
| `DEEPSEEK_API_KEY` | DeepSeek API key | DeepSeek |
|
|
100
|
+
| `DASHSCOPE_API_KEY` | Alibaba Cloud Model Studio key | Qwen |
|
|
101
|
+
| `BRAVE_API_KEY` | Brave Search API key (optional — enables WebSearch tool) | WebSearch |
|
|
28
102
|
|
|
29
103
|
Set your key before running:
|
|
30
104
|
|
|
@@ -35,22 +109,26 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
35
109
|
## Development Setup (Monorepo)
|
|
36
110
|
|
|
37
111
|
```bash
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
# Edit .env and set ANTHROPIC_API_KEY=sk-ant-...
|
|
41
|
-
|
|
42
|
-
# 2. Build dependencies and CLI
|
|
43
|
-
pnpm build:deps
|
|
44
|
-
pnpm --filter @robota-sdk/agent-cli build
|
|
112
|
+
# Build all packages, including the CLI and its web monitor
|
|
113
|
+
pnpm build
|
|
45
114
|
```
|
|
46
115
|
|
|
116
|
+
`pnpm build` runs each package's own build in dependency order. The CLI build runs `tsdown` and then
|
|
117
|
+
copies the web monitor (`agent-cli-web/dist`) into `dist/web`. To see the published tarball, run
|
|
118
|
+
`pnpm --filter @robota-sdk/agent-cli pack`.
|
|
119
|
+
|
|
120
|
+
Standalone Bun binaries are written to `dist-bun` (`pnpm --filter @robota-sdk/agent-cli build:bun`) and
|
|
121
|
+
`dist-bun-headless` (`build:bun:headless`). Native dependencies make these builds exact-host: each
|
|
122
|
+
supported Linux x64/arm64, macOS x64/arm64, or Windows x64 binary must be compiled on its matching host,
|
|
123
|
+
and a mismatched target is refused.
|
|
124
|
+
|
|
47
125
|
## Usage (Monorepo)
|
|
48
126
|
|
|
49
127
|
```bash
|
|
50
128
|
# From monorepo root
|
|
51
129
|
cd packages/agent-cli
|
|
52
130
|
|
|
53
|
-
# Development mode (no build needed
|
|
131
|
+
# Development mode (no build needed)
|
|
54
132
|
pnpm dev
|
|
55
133
|
|
|
56
134
|
# Production mode (requires build)
|
|
@@ -70,74 +148,618 @@ robota "prompt" # REPL with initial prompt
|
|
|
70
148
|
robota -p "prompt" # Print mode (one-shot, exit after response)
|
|
71
149
|
robota -c # Continue last session
|
|
72
150
|
robota -r <session-id> # Resume session by ID
|
|
73
|
-
robota --
|
|
151
|
+
robota --language <lang> # Response language (ko, en, ja, zh)
|
|
74
152
|
robota --permission-mode <mode> # plan | default | acceptEdits | bypassPermissions
|
|
75
153
|
robota --max-turns <n> # Limit agentic turns per interaction
|
|
154
|
+
robota --goal "<objective>" # Pursue an autonomous goal headlessly until satisfied or a bound
|
|
155
|
+
robota --goal-max-iterations <n> # Per-goal turn budget (default 25)
|
|
156
|
+
robota --output-format <fmt> # text | json | stream-json (print mode)
|
|
157
|
+
robota --effort <level> # auto | none | minimal | low | medium | high | xhigh | max
|
|
158
|
+
robota --system-prompt <text> # Replace system prompt (print mode)
|
|
159
|
+
robota --append-system-prompt <text> # Append to system prompt (print mode)
|
|
160
|
+
robota --model claude-sonnet-4-6 # Override provider model for this session
|
|
161
|
+
robota --allowed-tools "Bash,Read" # Whitelist specific tools
|
|
162
|
+
robota --denied-tools "Bash,Write" # Blacklist specific tools (denied > allowed)
|
|
163
|
+
robota --screen-reader # Screen-reader mode: no chrome, no motion, numbered menus, role labels
|
|
164
|
+
robota --no-screen-reader # Force it off for this run, whatever the env or settings say
|
|
165
|
+
# Pacing (ms): ROBOTA_SCREEN_READER_STARTUP_QUIET_MS=900 ROBOTA_SCREEN_READER_PREPARK_MS=50 (0 disables either)
|
|
166
|
+
robota --serve # Run as a headless runtime host over a loopback WS sidecar (used by the desktop GUI)
|
|
167
|
+
robota mcp serve # Serve one session to a local MCP client over stdio
|
|
168
|
+
robota mcp serve --http-token-file /absolute/private/path/mcp-token --http-port 8765
|
|
169
|
+
# Serve Streamable HTTP on 127.0.0.1; the token file must not exist
|
|
170
|
+
robota mcp serve --http-public-url https://agents.example.com/robota/mcp --oauth-issuer https://auth.example.com \
|
|
171
|
+
--oauth-scopes mcp:use --oauth-allowed-subjects alice@example.com
|
|
172
|
+
# Serve remote HTTP behind a proxy, admitting OAuth access tokens
|
|
173
|
+
robota trust status # Inspect canonical workspace trust
|
|
174
|
+
robota trust --yes # Grant trust for the current Git workspace
|
|
175
|
+
robota trust revoke --yes # Revoke the current workspace grant
|
|
176
|
+
robota usage # Show the last 7 days of personal usage from local session history
|
|
177
|
+
robota usage --period 30d # Show complete buckets for the last 30 calendar days
|
|
178
|
+
robota usage --timezone UTC --format json # Emit the versioned JSON projection
|
|
179
|
+
robota usage export --endpoint http://127.0.0.1:4318 # Send stored-usage Gauges to a loopback OTLP collector
|
|
180
|
+
robota usage export --signal traces --endpoint http://127.0.0.1:4318 # Send recorded prompt/provider/tool spans
|
|
181
|
+
robota usage export --signal logs --endpoint http://127.0.0.1:4318 # Send content-free completion events
|
|
182
|
+
robota --safe-mode # Every customization off, to rule one out
|
|
183
|
+
robota --reset # Delete user settings and exit
|
|
184
|
+
robota --check-update # Check npm for a newer CLI version and exit
|
|
185
|
+
robota --disable-update-check # Skip interactive startup update check for this run
|
|
76
186
|
robota --version # Show version
|
|
187
|
+
robota --reduced-motion # Suppress animation for this run (colour is unaffected)
|
|
188
|
+
robota --no-reduced-motion # Allow animation, overriding a persisted reducedMotion
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Personal Usage
|
|
192
|
+
|
|
193
|
+
`robota usage` reads local user and trusted-project session stores without starting a provider or
|
|
194
|
+
requiring network access. It reports sessions, started turns, tokens, cost confidence, model/provider/
|
|
195
|
+
surface/source breakdowns, privacy-safe activity counts, and coverage diagnostics. The project copy
|
|
196
|
+
wins when the same session ID exists in both stores. Stored prompts, responses, paths, and tool
|
|
197
|
+
payloads are never printed.
|
|
198
|
+
|
|
199
|
+
Use `--period 7d` (the default) or `--period 30d`, choose an IANA timezone with `--timezone`, and use
|
|
200
|
+
`--format json` for the external `schemaVersion: 1` projection. An empty store produces an empty
|
|
201
|
+
report; a supplied store set containing no readable records exits with an error instead of silently
|
|
202
|
+
reporting zero usage.
|
|
203
|
+
|
|
204
|
+
`robota usage export` is a separate, explicit, one-shot network action over OTLP/HTTP JSON to a
|
|
205
|
+
loopback collector (`127.0.0.1` or `[::1]`) only. The default `metrics` signal sends a current
|
|
206
|
+
**Gauge snapshot** of stored session/turn counts, tokens, estimated known USD cost, and unknown-cost
|
|
207
|
+
counts to `/v1/metrics`. Repeated exports are snapshots, not new usage to add together. Select
|
|
208
|
+
`--signal traces` to send recorded prompt-root, provider-call, and tool spans to `/v1/traces`, or
|
|
209
|
+
`--signal logs` to send recorded content-free completion events to `/v1/logs`. Repeating a logs export
|
|
210
|
+
can resend the same events; a collector may retain duplicates. These signals use stored records;
|
|
211
|
+
they are not live tracing, and legacy records may lack span/event coverage. No signal
|
|
212
|
+
exports prompt or tool bodies, credentials, or a remote destination. An unreadable stored session,
|
|
213
|
+
collector rejection, or network error fails the command without reporting success.
|
|
214
|
+
|
|
215
|
+
Live prompt telemetry is a separate, opt-in Node CLI feature (interactive, print, serve, and MCP serve). Set
|
|
216
|
+
`ROBOTA_TELEMETRY_ENABLED=1`, `ROBOTA_TELEMETRY_TRACES=otlp`,
|
|
217
|
+
`ROBOTA_TELEMETRY_OTLP_PROTOCOL=http/protobuf`, and
|
|
218
|
+
`ROBOTA_TELEMETRY_OTLP_ENDPOINT=https://collector.example` to send content-free prompt/provider/tool
|
|
219
|
+
spans to the base URL's `/v1/traces`. `ROBOTA_TELEMETRY_OTLP_TRACES_ENDPOINT` overrides the base with
|
|
220
|
+
an exact traces URL. Select `ROBOTA_TELEMETRY_METRICS=otlp` independently to send per-invoked-call
|
|
221
|
+
delta counts, complete-usage token totals and price-table-estimated USD cost, plus observed prompt and
|
|
222
|
+
tool-completion counts, to `/v1/metrics`;
|
|
223
|
+
`ROBOTA_TELEMETRY_OTLP_METRICS_ENDPOINT` overrides that destination. Missing usage or prices are
|
|
224
|
+
counted separately, never treated as zero cost. A truncated provider-event batch reports omissions
|
|
225
|
+
and does not claim a complete usage/cost total. Metric datapoints omit session, turn, provider and
|
|
226
|
+
model labels by default; set `ROBOTA_TELEMETRY_METRIC_ATTRIBUTES` to a comma list drawn from
|
|
227
|
+
`session`, `provider` and `model` (canonical lower case, no duplicates) to add them, and only when
|
|
228
|
+
metrics export over `otlp` or `console` — the setting is refused otherwise, and startup is refused
|
|
229
|
+
for any token that is not exactly one of the three. `session` adds `robota.session.id` (the same key
|
|
230
|
+
the trace spans use) to every metric datapoint of the batch. `provider`/`model` add
|
|
231
|
+
`robota.provider.id`/`robota.model.id` only to provider-derived metrics (calls, tokens, cost, and the
|
|
232
|
+
unpriced/usage-unavailable counts), splitting them into one datapoint per distinct id (or pair); a
|
|
233
|
+
call with no id gets its own datapoint without that attribute. In `--serve`/`robota mcp serve`, how
|
|
234
|
+
many distinct session ids appear is set by the connecting clients, not by the CLI; provider and model
|
|
235
|
+
values come from whatever the host's provider configuration reports, not from a fixed catalog. Select
|
|
236
|
+
`ROBOTA_TELEMETRY_LOGS=otlp` independently for content-free
|
|
237
|
+
prompt/provider/tool completion events, plus a tool's own permission decision (allowed, denied, or
|
|
238
|
+
hook-blocked), at `/v1/logs`; `ROBOTA_TELEMETRY_OTLP_LOGS_ENDPOINT`
|
|
239
|
+
overrides that destination. Tool spans and logs carry a validated opaque call ID when available, and an
|
|
240
|
+
invoked provider-call span and its completion log carry the provider's own request ID the same way when
|
|
241
|
+
the adapter attested one; metric datapoints never use either as a label, though a permission-decision
|
|
242
|
+
count by decision value is still reported on `/v1/metrics`. A permission decision has no duration of
|
|
243
|
+
its own and never produces a trace span. Only confirmed invocations produce provider-completion events;
|
|
244
|
+
omitted child counts remain visible on the prompt event. Plain HTTP is allowed only for loopback; URL credentials and query parameters
|
|
245
|
+
are rejected. Export is bounded, best-effort, and does not delay or fail a turn; delivery failures
|
|
246
|
+
produce a content-free stderr warning. These switches do not enable content capture,
|
|
247
|
+
additional event kinds, or replay of stored traces. Ambient `OTEL_*` values alone do not enable them.
|
|
248
|
+
|
|
249
|
+
Prompt, response and tool content is a separate opt-in on top of `ROBOTA_TELEMETRY_LOGS=otlp`.
|
|
250
|
+
`ROBOTA_TELEMETRY_LOG_USER_PROMPTS=1` sends what you typed (never the expanded model input, such as
|
|
251
|
+
`@file` contents), and `ROBOTA_TELEMETRY_LOG_ASSISTANT_RESPONSES=1` sends the assistant's final answer
|
|
252
|
+
for the turn. `ROBOTA_TELEMETRY_LOG_TOOL_ARGUMENTS=1` sends the arguments of the turn's tool calls,
|
|
253
|
+
allowed or denied, and `ROBOTA_TELEMETRY_LOG_TOOL_OUTPUT=1` sends the output of its allowed calls
|
|
254
|
+
(empty for a tool that crashed). Tool arguments carry whatever the model passed: a `Write` or `Edit`
|
|
255
|
+
call's arguments are the file content it writes. Each setting accepts exactly `0` or `1`.
|
|
256
|
+
`ROBOTA_TELEMETRY_LOG_CONTENT_MAX_BYTES` bounds each item (an integer from 256 to 16384, default 2048)
|
|
257
|
+
and is refused unless one of them is `1`. Content is captured only in the interactive terminal and
|
|
258
|
+
only for turns you type yourself, the same turns prompt history records: goal and loop wakeups, peer
|
|
259
|
+
and external messages, remote co-drivers, subagents and background work are never captured, and
|
|
260
|
+
tool content covers only the calls that turn made itself — never a subagent's, a forked skill's or
|
|
261
|
+
background work's, even through a tool they share. A hook-blocked call, an unknown tool, or a call
|
|
262
|
+
stopped before its tool ran sends nothing. Print (`-p`, `--goal`), `--serve` and
|
|
263
|
+
`robota mcp serve` refuse to start with a content setting at `1` rather than ignore it. It goes only to OTLP log records (`robota.content.captured`, joined to the prompt's trace and
|
|
264
|
+
root span, with `robota.content.kind`, `robota.content.truncated`, `robota.content.original_bytes` and,
|
|
265
|
+
for an interrupted turn's response, `robota.content.partial`) — never to spans, metrics or console
|
|
266
|
+
output, so a content setting with `ROBOTA_TELEMETRY_LOGS=console` is refused. A tool item is joined to
|
|
267
|
+
the call's tool span when the trace kept one (else the root span) and adds `robota.tool.call_id`,
|
|
268
|
+
`robota.tool.name` and `robota.tool.outcome` (`success`, `failure` or `denied`). Arguments are
|
|
269
|
+
rendered with values under secret-looking keys (`password`, `apiKey`, `accessTokens`, …) masked
|
|
270
|
+
whole, and binary or base64 payloads replaced by their size. It is sent to the logs
|
|
271
|
+
destination with its headers but in its own requests and queue: a content failure never delays or
|
|
272
|
+
drops the content-free logs, and is reported on stderr like any other delivery failure. One turn's
|
|
273
|
+
content is bounded in item count and total size, with room kept for the prompt and response, and is
|
|
274
|
+
sent as a few requests of bounded size; what does not fit is dropped and counted by kind in a
|
|
275
|
+
content-free `robota.content.omitted` record, sent last. When a request fails, the rest of that turn's
|
|
276
|
+
content and every queued turn are dropped, so that count is lost too. Queued content can hold about
|
|
277
|
+
12 MB in the worst case. Before sending, the CLI masks known credential shapes (vendor API keys, AWS keys, private-key blocks,
|
|
278
|
+
JWTs, GitHub, Stripe, npm and GitLab tokens, bearer tokens, URL and `-u user:pass` credentials,
|
|
279
|
+
`*_KEY`/`*_TOKEN`/`*_SECRET`/`*_PASSWORD=` values, JSON values whose name looks secret, and
|
|
280
|
+
`Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie` and `X-…-Token`/`-Key`/`-Secret`/`-Auth`
|
|
281
|
+
header lines, including indented ones and the `> ` lines `curl -v` prints), the literal secrets it knows of (settings keys and
|
|
282
|
+
`env` values, every resolved provider key including one switched to mid-session, and collector header
|
|
283
|
+
values), your workspace path (`<workspace>`, also inside `file://` URLs) and home directory (`~`),
|
|
284
|
+
control characters, and a partial token left at the size cut. This
|
|
285
|
+
masking is best effort: anything it does not recognise as a secret is sent as written — file
|
|
286
|
+
contents, command output and a response that repeats them included. If the secrets cannot be
|
|
287
|
+
read, that request carries no content at all.
|
|
288
|
+
|
|
289
|
+
Static collector headers (for example an `Authorization` token) use
|
|
290
|
+
`ROBOTA_TELEMETRY_OTLP_HEADERS` for the generic endpoint and `ROBOTA_TELEMETRY_OTLP_TRACES_HEADERS`,
|
|
291
|
+
`ROBOTA_TELEMETRY_OTLP_METRICS_HEADERS` or `ROBOTA_TELEMETRY_OTLP_LOGS_HEADERS` for one signal, in
|
|
292
|
+
OpenTelemetry's `name=value,name2=value2` form with percent-encoded values
|
|
293
|
+
(`Authorization=Bearer%20abc123`). Headers are scoped to their destination: a signal that uses
|
|
294
|
+
`ROBOTA_TELEMETRY_OTLP_ENDPOINT` sends the generic headers merged with its own, its own winning on the
|
|
295
|
+
same name, while a signal with its own `ROBOTA_TELEMETRY_OTLP_<SIGNAL>_ENDPOINT` sends only its own
|
|
296
|
+
headers. Unlike OpenTelemetry, generic headers are never sent to a per-signal endpoint. Startup is
|
|
297
|
+
refused for malformed entries, empty names or values, duplicate names, reserved transport,
|
|
298
|
+
content-negotiation, proxy, `sec-` or trace-propagation names, control characters other than tab, non-ASCII characters,
|
|
299
|
+
oversized settings, headers no OTLP signal would send, and a per-signal endpoint without its own
|
|
300
|
+
headers while another signal sends the generic ones. Header helpers and refresh are not supported.
|
|
301
|
+
Console output, logs and resource attributes never contain headers, and errors name only the setting
|
|
302
|
+
and entry position. The CLI removes every `ROBOTA_TELEMETRY_*` setting from its environment at
|
|
303
|
+
startup, so shells, hooks, subagents and other child processes do not inherit them; the only
|
|
304
|
+
handover is the supervised runtime that `session start` or `session view` launches, which receives
|
|
305
|
+
them in its spawn environment. While telemetry is enabled, any other `ROBOTA_TELEMETRY_*` setting
|
|
306
|
+
(for example client certificates, a locked destination or other content capture) stops startup with an
|
|
307
|
+
error that names the setting but never prints its value, rather than exporting without it.
|
|
308
|
+
Each signal also accepts `console` instead of `otlp` to write a content-free JSON diagnostic to stderr;
|
|
309
|
+
console needs neither an endpoint nor a protocol and never includes collector credentials. Signals
|
|
310
|
+
remain independent, and the Robota enable switch is still required.
|
|
311
|
+
All enabled signals use the same per-process `service.instance.id` and the CLI's version and
|
|
312
|
+
presentation mode as resource attributes. Ambient `OTEL_SERVICE_NAME` and other `OTEL_*` values
|
|
313
|
+
cannot replace these fields.
|
|
314
|
+
|
|
315
|
+
Trace context propagation is a further opt-in. `ROBOTA_TELEMETRY_PROPAGATE_TO` is a comma list of
|
|
316
|
+
exact origins (`https://api.anthropic.com,https://gateway.example.com:8443`) that may receive a
|
|
317
|
+
W3C `traceparent` on provider requests: `00-<prompt trace id>-<provider-call span id>-01`, where the
|
|
318
|
+
span ID is the one the exported `robota.provider_call` span carries. The same list covers MCP servers
|
|
319
|
+
reached over Streamable HTTP: a tool call to a server whose URL has a listed origin carries
|
|
320
|
+
`00-<prompt trace id>-<tool span id>-01`, where the span ID is the one the exported tool span
|
|
321
|
+
carries. Only the `tools/call` request and its cancellation carry it — never initialization,
|
|
322
|
+
listing, list refreshes, notifications or the server's event stream — and stdio MCP servers receive
|
|
323
|
+
nothing. It needs
|
|
324
|
+
`ROBOTA_TELEMETRY_ENABLED=1` and `ROBOTA_TELEMETRY_TRACES=otlp` or `console`, and is inert while
|
|
325
|
+
telemetry is off. Each entry must be exactly its own origin: `https`, or `http` only on loopback, with no
|
|
326
|
+
path, trailing slash, query, credentials, wildcard or spelled-out default port; a scheme, port or
|
|
327
|
+
subdomain difference is a different origin and gets nothing. An internationalized host must be listed
|
|
328
|
+
in its punycode (`xn--`) form, and an origin with a trailing dot never matches; both fail closed
|
|
329
|
+
(refused at startup or sent nothing). Malformed, duplicate or too many entries
|
|
330
|
+
stop startup with an error that names only the setting and entry position. `tracestate` and `baggage`
|
|
331
|
+
are never sent, the collector's origin is never trusted implicitly, and collector headers are never
|
|
332
|
+
reused for provider requests. The Anthropic and OpenAI (Responses and Chat Completions) adapters
|
|
333
|
+
propagate to their client's effective base URL, as do the OpenAI-compatible DeepSeek, Qwen (both its
|
|
334
|
+
Chat Completions and Responses surfaces), and Gemma adapters, and Gemini propagates to
|
|
335
|
+
`https://generativelanguage.googleapis.com` only — not with `GOOGLE_GEMINI_BASE_URL`,
|
|
336
|
+
`GOOGLE_VERTEX_BASE_URL` or Vertex mode. Nothing is sent through a provider executor. When
|
|
337
|
+
propagation is configured but the round's provider cannot propagate — an executor, or a client
|
|
338
|
+
whose base URL cannot be read — the CLI writes one stderr line per provider naming only that
|
|
339
|
+
provider. Only a prompt's own provider calls carry it: subagents, workers and background
|
|
340
|
+
tasks do not inherit it.
|
|
341
|
+
|
|
342
|
+
Listing a vendor's origin lets that vendor link its own request logs to your trace ID. A redirect
|
|
343
|
+
followed by the SDK carries the header to the redirect target. A provider call whose span was omitted or
|
|
344
|
+
dropped from export still sent its `traceparent`, so the vendor's parent span may be missing from your
|
|
345
|
+
trace; `robota.omitted.provider_count` on the prompt span shows when that happened. Ambient
|
|
346
|
+
`TRACEPARENT` and `OTEL_*` values are never adopted.
|
|
347
|
+
|
|
348
|
+
`ROBOTA_TELEMETRY_PROPAGATE_TO_SUBPROCESSES` hands the prompt's trace to child processes through the
|
|
349
|
+
`TRACEPARENT` environment variable. It is a comma list drawn from exactly `shell` and `hooks`, each at
|
|
350
|
+
most once; it needs the same `ROBOTA_TELEMETRY_ENABLED=1` and exported traces, is inert while
|
|
351
|
+
telemetry is off, works with or without `ROBOTA_TELEMETRY_PROPAGATE_TO`, and a malformed entry stops
|
|
352
|
+
startup naming only the setting and entry position. With `shell`, each foreground `Bash`/`Shell`
|
|
353
|
+
command runs with `00-<prompt trace id>-<tool span id>-01`, where the span ID is the one that call's
|
|
354
|
+
exported tool span carries. With `hooks`, command hooks fired during a prompt — `UserPromptSubmit`,
|
|
355
|
+
`PreToolUse`, `PostToolUse`, `PermissionDecision`, the model-call hooks, `Stop`, `StopFailure` and
|
|
356
|
+
the `PreCompact` and `PostCompact` of an automatic compaction — run with `00-<prompt trace id>-<prompt span id>-01`, so
|
|
357
|
+
their spans sit beside the provider and tool spans; a hook fired outside a prompt (`SessionStart`,
|
|
358
|
+
`SessionEnd`, both hooks of `/compact`, background tasks, subagent worktrees) gets nothing. The value
|
|
359
|
+
is only ever in the child's environment, never in a hook's stdin JSON. A `TRACEPARENT` that a hook
|
|
360
|
+
group's own `env` sets wins, and the child then sees its environment unchanged; otherwise the ambient
|
|
361
|
+
`TRACESTATE` is removed, because it belonged to a different parent. The `!` shell passthrough,
|
|
362
|
+
background, managed and scheduled shells, the monitor UI launcher, a sandboxed shell, stdio MCP
|
|
363
|
+
servers, and HTTP, prompt and agent hooks never receive it. Robota never modifies its own process
|
|
364
|
+
environment, so while the setting is off every child sees exactly the ambient `TRACEPARENT` and
|
|
365
|
+
`TRACESTATE` it would have seen anyway.
|
|
366
|
+
|
|
367
|
+
Upgrading: an origin already listed for a provider now also sends `traceparent` to an MCP HTTP server
|
|
368
|
+
at that exact origin. Remove the origin, or move the MCP server to a different origin, if that server
|
|
369
|
+
should not link its logs to your trace.
|
|
370
|
+
|
|
371
|
+
### Doctor
|
|
372
|
+
|
|
373
|
+
`robota doctor` (aliases: `checkup`, `diagnose`) diagnoses configuration and runtime readiness
|
|
374
|
+
before any session exists, so a broken configuration cannot make the diagnostic unreachable. It reports
|
|
375
|
+
every settings layer in precedence order with its state and cause, the merged keys with the layer that
|
|
376
|
+
contributed each, provider resolution and endpoint reachability, workspace trust, storage, plugins,
|
|
377
|
+
skills, hooks and MCP declarations — naming the exact file and cause, never a credential. Exit code
|
|
378
|
+
`0` means no check failed (warnings allowed); `1` means at least one did.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
robota doctor # full report
|
|
382
|
+
robota doctor --repair settings.user.robota # one allowlisted repair, asks [y/N] first
|
|
383
|
+
robota doctor --repair storage.user --yes # no prompt (required in a non-interactive shell)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Repairs are limited to an empty user settings file (rewritten as `{}`) and a missing or too-open
|
|
387
|
+
user storage directory; everything else is reported with the path to fix. `/doctor` runs the same
|
|
388
|
+
report inside a session, and `/doctor repair <check-id>` asks before writing.
|
|
389
|
+
|
|
390
|
+
### MCP Servers
|
|
391
|
+
|
|
392
|
+
#### Serve Robota to an MCP host
|
|
393
|
+
|
|
394
|
+
Install `@robota-sdk/agent-cli`, configure a provider with `robota --configure`, and grant the
|
|
395
|
+
intended project with `robota trust --yes` before starting a headless server. Resolve the actual
|
|
396
|
+
executable (`command -v robota`) and use its **absolute path** in the host configuration. For an
|
|
397
|
+
MCP client that supports a child-process working directory, configure:
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"mcpServers": {
|
|
402
|
+
"robota": {
|
|
403
|
+
"command": "/absolute/path/to/robota",
|
|
404
|
+
"args": ["mcp", "serve"],
|
|
405
|
+
"cwd": "/absolute/path/to/trusted/project"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The host must launch the process in the intended project directory. Robota uses that inherited
|
|
412
|
+
directory for its normal project-root, access and trust decision; an MCP `roots/list` value or an
|
|
413
|
+
environment variable does not silently change it. For Claude Code, whose stdio server environment
|
|
414
|
+
includes `CLAUDE_PROJECT_DIR`, a project-scoped launch command can select that directory explicitly:
|
|
415
|
+
|
|
416
|
+
```sh
|
|
417
|
+
claude mcp add --scope project --transport stdio robota -- \
|
|
418
|
+
/bin/sh -c 'cd "$CLAUDE_PROJECT_DIR" && exec /absolute/path/to/robota mcp serve'
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The host receives the canonical runtime tool catalog plus `robota_submit`. Model-invocable commands
|
|
422
|
+
appear only through their canonical `robota_command_*` names. Tools run through the session's normal
|
|
423
|
+
permission and hook policy; denied or approval-requiring calls return MCP tool errors, never an
|
|
424
|
+
interactive prompt on the protocol stream. The peer is a local process started by the user and can
|
|
425
|
+
request actions within that session's admitted workspace and permissions. This mode starts no web
|
|
426
|
+
server, WebSocket sidecar or TUI. Stdout carries only MCP messages; startup notices and failures go
|
|
427
|
+
to stderr. Closing the host's stdin or sending SIGINT/SIGTERM shuts down the carrier and session.
|
|
428
|
+
If the host reports a connection failure, run `robota trust status` and `robota doctor` separately in
|
|
429
|
+
the same project directory, then inspect the host's captured stderr for configuration errors.
|
|
430
|
+
|
|
431
|
+
The same session can also use an admitted external MCP tool while it serves the host. Its connected
|
|
432
|
+
client tools appear in the served catalog under canonical names such as `probe__echo`; they still
|
|
433
|
+
run through the session's normal permission policy. The server carrier and outbound client close
|
|
434
|
+
independently. An embedding host can supply its own `IMCPActivationApprovalStore` through
|
|
435
|
+
`startCli({ mcpApprovalStore })` before startup, along with an explicitly approved
|
|
436
|
+
`mcpHttpTransportDeps` egress policy when needed. Neither capability comes from MCP settings or the
|
|
437
|
+
remote caller. The ordinary `robota` executable supplies neither automatically.
|
|
438
|
+
|
|
439
|
+
#### Serve Robota to a remote MCP client
|
|
440
|
+
|
|
441
|
+
`--http-token-file` is for clients on the same machine: it binds only `127.0.0.1`, and its bearer
|
|
442
|
+
is never accepted beyond loopback. To serve a client elsewhere, run Robota as an OAuth resource
|
|
443
|
+
server behind an HTTPS reverse proxy. An authorization server you already run issues the access
|
|
444
|
+
tokens; Robota only verifies them.
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
robota mcp serve \
|
|
448
|
+
--http-public-url https://agents.example.com/robota/mcp \
|
|
449
|
+
--oauth-issuer https://auth.example.com \
|
|
450
|
+
--oauth-scopes mcp:use \
|
|
451
|
+
--oauth-allowed-subjects alice@example.com \
|
|
452
|
+
--http-host 127.0.0.1 --http-port 8765 \
|
|
453
|
+
--trusted-proxy 127.0.0.1
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
- `--http-public-url` is the `https` address clients use. It is also the token audience and the
|
|
457
|
+
`resource` Robota advertises. Robota serves MCP at its path (`/robota/mcp` above) and the RFC 9728
|
|
458
|
+
protected-resource metadata at `/.well-known/oauth-protected-resource` followed by that path. The
|
|
459
|
+
proxy must forward both paths unchanged (do not strip the prefix) and must preserve the client's
|
|
460
|
+
`Host` header. Robota checks `Host` and `Origin` against the public URL, not the address it binds.
|
|
461
|
+
- `--oauth-issuer`, `--oauth-scopes` and `--oauth-allowed-subjects` are all required. A token must
|
|
462
|
+
be an RFC 9068 access token from that issuer, addressed to the public URL, carrying every listed
|
|
463
|
+
scope, and issued to a listed subject. Every admitted client drives the same session, so name only
|
|
464
|
+
the people you would hand this terminal to.
|
|
465
|
+
- `--http-host` defaults to `127.0.0.1`, which suits a proxy on the same machine. Robota binds any
|
|
466
|
+
other address only when all the flags above are present.
|
|
467
|
+
- A request without a valid token receives `401` with
|
|
468
|
+
`WWW-Authenticate: Bearer resource_metadata="…"`, and a token missing a scope receives `403`
|
|
469
|
+
with `insufficient_scope`. The body is always empty. MCP clients that support authorization
|
|
470
|
+
use that challenge to discover the authorization server.
|
|
471
|
+
- Failed requests are counted per client address. After too many failures in a minute, that
|
|
472
|
+
address receives `429`; a valid token is never throttled. The client address is read from
|
|
473
|
+
`X-Forwarded-For` only when the connection comes from a `--trusted-proxy` address (repeatable).
|
|
474
|
+
- Each refusal is logged on stderr as a reason and an address class (`loopback`, `private`,
|
|
475
|
+
`public`), never the token or the address itself.
|
|
476
|
+
- The server is stateless: it issues no `Mcp-Session-Id`, so there are no sessions to enumerate or
|
|
477
|
+
hijack.
|
|
478
|
+
|
|
479
|
+
The settings described below configure Robota as an MCP **client**.
|
|
480
|
+
|
|
481
|
+
Declare remote MCP servers under an `mcpServers` key in any layered settings file (managed, user, or
|
|
482
|
+
project `.robota`/`.claude` settings) — the same precedence order every other setting uses. Each
|
|
483
|
+
entry names a `"type": "http"` transport and a `url`; `${VAR}`/`${VAR:-default}` references in `url`,
|
|
484
|
+
`headers`, and `env` are resolved from the process environment. Every declared server is
|
|
485
|
+
**deny-by-default**: a server must be explicitly approved with `/mcp approve <serverId>` before its
|
|
486
|
+
tools are connected. Use `/mcp` (or `/mcp list`) to see every declared server's admission status,
|
|
487
|
+
and `/mcp reject`/`/mcp revoke` to withdraw approval. Only approved servers already known at startup
|
|
488
|
+
are connected. Approval is in-memory for the ordinary executable, so `/mcp approve` mid-session
|
|
489
|
+
records a decision for that session but does not connect the server in the running session or
|
|
490
|
+
persist it across a restart. An embedding host can preserve approval state across starts by
|
|
491
|
+
supplying the same store; it remains responsible for when to reconnect approved definitions.
|
|
492
|
+
|
|
493
|
+
A remote server that declares `"oauth": {}` (optionally with `clientId`, `callbackPort`,
|
|
494
|
+
`authServerMetadataUrl` and `scopes`) needs its own sign-in before a session can use it. Sign-in is
|
|
495
|
+
per server, naming the server — inside a session with `/mcp login <server>`, or from a terminal:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
robota mcp login files # opens your browser; tokens kept owner-only in ~/.robota/mcp-credentials
|
|
499
|
+
robota mcp login files --no-browser # prints the URL; paste back the address your browser was sent to
|
|
500
|
+
robota mcp login files --client-secret # a pre-registered confidential client: asks for the secret
|
|
501
|
+
robota mcp logout files # deletes the stored tokens, then revokes them where the server allows
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Use `--no-browser` when the browser runs on another machine (for example over SSH). After you
|
|
505
|
+
approve, the browser is sent to a `http://127.0.0.1:<port>/callback` address that may not load; copy
|
|
506
|
+
that full address and paste it at the prompt (the input is not echoed). It is accepted only if it is
|
|
507
|
+
this sign-in's redirect address and carries its `state`, and only within the same five minutes the
|
|
508
|
+
browser flow allows.
|
|
509
|
+
|
|
510
|
+
`robota mcp logout <server>` always deletes that server's local credential, even when the
|
|
511
|
+
authorization server cannot be reached or refuses to revoke the tokens; it then says, per token,
|
|
512
|
+
whether it was revoked, and otherwise why not (by a short reason) or that the server offers no
|
|
513
|
+
revocation. `/mcp` shows each OAuth server's sign-in state — `signed in`, `token expired, will
|
|
514
|
+
refresh`, `sign-in required` or `signed out` — never a token, and for a server that needs a sign-in
|
|
515
|
+
it names `/mcp login <server>` and `robota mcp login <server>` (the server's name appears in them
|
|
516
|
+
only when it is a plain name that is safe to paste into any shell). `/mcp logout <serverId>` signs
|
|
517
|
+
out of that one server from inside a session and stops the session sending the token it holds.
|
|
518
|
+
|
|
519
|
+
`/mcp login <server>` signs in without leaving the session: it first shows the authorization URL,
|
|
520
|
+
where you can choose to open your browser, paste the redirect instead, or cancel. When no browser
|
|
521
|
+
can be opened — or with `/mcp login <server> --no-browser` — it asks for the redirect address in the
|
|
522
|
+
session's own prompt (the input is not shown). Once signed
|
|
523
|
+
in, a server that could not connect at startup is admitted as usual (it must be approved) and
|
|
524
|
+
connected, and its tools are available from your next message; a server whose sign-in lapsed
|
|
525
|
+
mid-session works again. A failed or cancelled sign-in changes nothing. A client secret is never
|
|
526
|
+
typed into a session: for a pre-registered client that needs one, run
|
|
527
|
+
`robota mcp login <server> --client-secret` in a terminal.
|
|
528
|
+
|
|
529
|
+
### MCP Background Handoff
|
|
530
|
+
|
|
531
|
+
A slow MCP tool call can be handed to a background task instead of blocking the turn. Configure it
|
|
532
|
+
under an `mcp` key (beside `mcpServers`, in the same layered settings files):
|
|
533
|
+
|
|
534
|
+
```json
|
|
535
|
+
{
|
|
536
|
+
"mcp": {
|
|
537
|
+
"autoBackgroundMs": 120000,
|
|
538
|
+
"callTimeoutMs": 600000
|
|
539
|
+
}
|
|
540
|
+
}
|
|
77
541
|
```
|
|
78
542
|
|
|
543
|
+
- `autoBackgroundMs` (default 120000 ms): a tool call still running at this point is handed to a
|
|
544
|
+
`tool-invocation` background task; `/tasks` shows it like any other background task. `0` disables
|
|
545
|
+
the handoff.
|
|
546
|
+
- `callTimeoutMs` (default 600000 ms): the tool call's own budget, enforced by the MCP client in
|
|
547
|
+
every mode — including print. `autoBackgroundMs` must be less than `callTimeoutMs`, or the handoff
|
|
548
|
+
is disabled with a warning.
|
|
549
|
+
- The handoff applies to the interactive TUI and `robota --serve`. In print mode (`-p`) a slow MCP call always runs to completion in the
|
|
550
|
+
foreground, bounded by `callTimeoutMs`; a positive `autoBackgroundMs` is reported as ignored there.
|
|
551
|
+
|
|
552
|
+
### CLI Updates
|
|
553
|
+
|
|
554
|
+
Robota can check npm for a newer `@robota-sdk/agent-cli` version:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
robota --check-update
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
When an update is available, Robota prints the npm global install command:
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
npm install -g '@robota-sdk/agent-cli@latest'
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Robota does not implement its own updater and does not modify `~/.robota/settings.json` for update checks. Interactive startup checks use a user-level operational cache at `~/.robota/update-check.json` and can be skipped for one run with `--disable-update-check`. Print/headless mode (`robota -p`) does not perform automatic startup update checks so scripted stdout and stderr remain deterministic.
|
|
567
|
+
|
|
568
|
+
### Print Mode Output Formats
|
|
569
|
+
|
|
570
|
+
Print mode (`-p`) supports three output formats via `--output-format`:
|
|
571
|
+
|
|
572
|
+
| Format | Description |
|
|
573
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
574
|
+
| `text` | Plain text response to stdout (default) |
|
|
575
|
+
| `json` | Single JSON object: `{ type, result, session_id, subtype }` |
|
|
576
|
+
| `stream-json` | Newline-delimited JSON with `content_block_delta` streaming events |
|
|
577
|
+
|
|
578
|
+
### Exit Codes (print mode)
|
|
579
|
+
|
|
580
|
+
| Code | Meaning |
|
|
581
|
+
| ---- | ---------------------------------------------- |
|
|
582
|
+
| 0 | Success |
|
|
583
|
+
| 1 | General error |
|
|
584
|
+
| 2 | Argument error |
|
|
585
|
+
| 3 | Configuration error (missing provider/API key) |
|
|
586
|
+
| 4 | API error |
|
|
587
|
+
| 5 | Tool execution error |
|
|
588
|
+
|
|
589
|
+
### Stdin Pipe
|
|
590
|
+
|
|
591
|
+
When stdin is piped, the CLI reads it automatically. If a positional prompt is also given, the piped content is appended inside `<stdin>` tags:
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
echo "Explain this error" | robota -p
|
|
595
|
+
cat file.ts | robota -p "Review this code" --output-format json
|
|
596
|
+
git diff | robota -p "Summarize changes" --output-format stream-json
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## First-Run Setup
|
|
600
|
+
|
|
601
|
+
When no usable settings file exists, the CLI prompts for:
|
|
602
|
+
|
|
603
|
+
1. **Provider selection** from the providers assembled into the CLI binary
|
|
604
|
+
2. **Provider-specific setup fields** such as model, base URL, and masked API key
|
|
605
|
+
3. **Response language** (ko/en/ja/zh, default: en)
|
|
606
|
+
|
|
607
|
+
Creates `~/.robota/settings.json`. Use `robota --reset` to return to first-run state.
|
|
608
|
+
|
|
609
|
+
Provider setup is generated from provider definitions. The default CLI build includes Anthropic,
|
|
610
|
+
OpenAI-compatible, DeepSeek, Gemma, and Qwen providers; other embeddings can inject their own
|
|
611
|
+
provider definitions.
|
|
612
|
+
Interactive setup creates a readable profile key from the selected model id, such as
|
|
613
|
+
`claude-sonnet-4-6` or `gpt-4o`, and appends `-2`, `-3`, etc. when that key already exists. Generated
|
|
614
|
+
profile keys never include API keys or credential hints.
|
|
615
|
+
|
|
616
|
+
Inside the TUI, `/provider` and `/provider list` show configured profiles as an interactive picker. Selecting a profile opens command-owned actions for switch, edit, test, duplicate, delete, and cancel. Headless mode prints the same profile list text without opening prompts.
|
|
617
|
+
|
|
618
|
+
Non-interactive/headless mode never prompts. Configure a provider ahead of time with `robota --configure` in an interactive terminal, or use `robota --configure-provider <profile> --type <type> ... --set-current`.
|
|
619
|
+
|
|
79
620
|
## Built-in Tools
|
|
80
621
|
|
|
81
|
-
The
|
|
622
|
+
The AI agent can invoke 9 distinct local tools (the runtime registers 10 tool names — `Bash` is the model-familiar alias of `Shell`):
|
|
82
623
|
|
|
83
|
-
| Tool
|
|
84
|
-
|
|
|
85
|
-
| `Bash`
|
|
86
|
-
| `Read`
|
|
87
|
-
| `Write`
|
|
88
|
-
| `Edit`
|
|
89
|
-
| `Glob`
|
|
90
|
-
| `Grep`
|
|
624
|
+
| Tool | Description | Primary Argument |
|
|
625
|
+
| ----------------- | ---------------------------------------------- | ---------------- |
|
|
626
|
+
| `Bash` | Execute shell commands | `command` |
|
|
627
|
+
| `Read` | Read file contents with line numbers | `filePath` |
|
|
628
|
+
| `Write` | Write content to a file | `filePath` |
|
|
629
|
+
| `Edit` | Replace a string in a file | `filePath` |
|
|
630
|
+
| `Glob` | Find files matching a pattern | `pattern` |
|
|
631
|
+
| `Grep` | Search file contents with regex | `pattern` |
|
|
632
|
+
| `WebFetch` | Fetch URL content as text | `url` |
|
|
633
|
+
| `WebSearch` | Search the internet (requires `BRAVE_API_KEY`) | `query` |
|
|
634
|
+
| `AskUserQuestion` | Ask the user structured questions mid-task | `questions` |
|
|
91
635
|
|
|
92
|
-
|
|
636
|
+
> **WebSearch** requires a `BRAVE_API_KEY` environment variable. Without it, the tool returns a setup message instead of results. Get a free key at [brave.com/search/api](https://brave.com/search/api/) (2,000 queries/month free tier).
|
|
93
637
|
|
|
94
|
-
|
|
638
|
+
## Recent TUI Capabilities
|
|
95
639
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
640
|
+
- Provider setup and profile management are generated from provider definitions, so the default CLI
|
|
641
|
+
build can configure, switch, edit, test, duplicate, and delete Anthropic, OpenAI-compatible,
|
|
642
|
+
DeepSeek, Gemma, and Qwen profiles without provider-specific UI branches.
|
|
643
|
+
- Interactive startup can check npm for newer CLI versions; print/headless mode skips startup update checks to keep scripted output deterministic.
|
|
644
|
+
- Long-running sessions show provider usage summaries, status activity, background job tree rows, and collapsed command-output transcripts.
|
|
645
|
+
- Edit results render as context hunks with markdown-friendly diff blocks.
|
|
646
|
+
- Background subagents are real runtime jobs with transcripts and resumable task snapshots.
|
|
647
|
+
- Explicit multi-agent requests use the `/agent` command module batch path through the SDK runtime.
|
|
99
648
|
|
|
100
|
-
|
|
649
|
+
### Recap when you come back
|
|
650
|
+
|
|
651
|
+
The TUI notices when you leave the terminal — by focus, where the terminal reports it (iTerm2,
|
|
652
|
+
Kitty, WezTerm, Alacritty, Ghostty, VS Code, Windows Terminal, tmux with `focus-events on`), or
|
|
653
|
+
after five minutes without a keystroke elsewhere — and on your return prints one line for the
|
|
654
|
+
interval, or nothing if nothing happened:
|
|
101
655
|
|
|
102
656
|
```
|
|
103
|
-
|
|
104
|
-
Arguments: command: rm -rf dist
|
|
105
|
-
Allow? [y/N]
|
|
657
|
+
While away 12m: 2 turns finished (1 wake) · 1 needs input · 1 failed
|
|
106
658
|
```
|
|
107
659
|
|
|
108
|
-
|
|
109
|
-
-
|
|
660
|
+
Every background row carries its state word beside the glyph (`working`, `needs-input`,
|
|
661
|
+
`completed`, `failed`, `stopped`), the one-line headline, and for a sleeping `/schedule` a live
|
|
662
|
+
`in 59s` countdown. `ROBOTA_FOCUS_EVENTS=0` turns focus reporting off (idle detection remains);
|
|
663
|
+
`=1` requests it even where the TUI would not.
|
|
110
664
|
|
|
111
|
-
|
|
665
|
+
### Prompt history
|
|
112
666
|
|
|
113
|
-
|
|
667
|
+
`Ctrl+R` searches every prompt you have typed — in this session, in this project, or anywhere —
|
|
668
|
+
the way a shell's reverse search does. Type to narrow the list (newest first, matches highlighted),
|
|
669
|
+
`Ctrl+S` cycles the scope `all → session → project`, `Enter` or `Tab` puts the highlighted prompt back
|
|
670
|
+
in the input, `Ctrl+E` runs it, `Esc` returns you to exactly the draft you had. Every key is
|
|
671
|
+
rebindable in `~/.robota/keybindings.json` (context `history-search`, and `chat-input.history-search`
|
|
672
|
+
for the opener).
|
|
114
673
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
674
|
+
What is written, where, and how to turn it off: the interactive TUI appends each prompt you submit
|
|
675
|
+
to `~/.robota/history.jsonl` (one JSON line — timestamp, session id, project root, text; readable by
|
|
676
|
+
you only). Prompts are already kept verbatim in the session record; this file is a searchable index
|
|
677
|
+
of them across sessions. `--serve` and print mode never write it. Set `"promptHistory": false` in
|
|
678
|
+
`~/.robota/settings.json` to turn it off, or `ROBOTA_PROMPT_HISTORY=0` for one run (`=1` overrides
|
|
679
|
+
the setting). Delete the file to forget everything.
|
|
121
680
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- **deny** — tool is blocked silently (no prompt shown)
|
|
681
|
+
Searching the conversation itself needs no viewer: the TUI never switches to the alternate screen,
|
|
682
|
+
so every message of a resumed session is in your terminal's own scrollback and search.
|
|
125
683
|
|
|
126
|
-
|
|
684
|
+
### Themes
|
|
127
685
|
|
|
128
|
-
|
|
686
|
+
`/theme` opens a picker: moving the highlight previews that theme in the live region — the input
|
|
687
|
+
frame, the status bar and the overlay itself — so you judge a colour scheme against the thing it
|
|
688
|
+
applies to rather than a swatch. `Enter` applies it, `Esc` leaves the previous one in place. The
|
|
689
|
+
transcript above keeps the colours it was written in, because the terminal owns those lines once
|
|
690
|
+
they are printed.
|
|
691
|
+
|
|
692
|
+
Four built-ins ship: `dark` (what Robota has always looked like), `light`, and `dark-daltonized` /
|
|
693
|
+
`light-daltonized`, which avoid the red/green distinction entirely — blue for "good", orange for
|
|
694
|
+
"bad" — for the roughly 1 in 12 men with a colour-vision deficiency. The daltonized pair is checked
|
|
695
|
+
mechanically: a test simulates protanopia and deuteranopia over the pairs whose difference in colour
|
|
696
|
+
carries meaning and fails if any of them come too close.
|
|
697
|
+
|
|
698
|
+
Without the picker: `/theme list` shows what is installed and what is active, `/theme <id>` switches,
|
|
699
|
+
`/theme syntax on|off` toggles code-block highlighting, and `/theme motion on|off` toggles animation.
|
|
700
|
+
All three persist to `~/.robota/settings.json` as the flat keys `theme`, `syntaxHighlighting` and
|
|
701
|
+
`reducedMotion`.
|
|
702
|
+
|
|
703
|
+
#### Writing your own
|
|
129
704
|
|
|
130
|
-
|
|
705
|
+
Drop a `.json` file in `~/.robota/themes/` and it appears in the list as `custom:<file-name>`. The
|
|
706
|
+
name becomes part of an id you type, so it may use up to 24 characters from letters, digits, `.`,
|
|
707
|
+
`-` and `_` — a file named anything else is skipped with a line saying so, rather than listed as a theme no command can apply:
|
|
131
708
|
|
|
709
|
+
```json
|
|
710
|
+
{
|
|
711
|
+
"name": "Mine",
|
|
712
|
+
"base": "light",
|
|
713
|
+
"overrides": { "colors": { "text": { "accent": "#56b4e9" } } }
|
|
714
|
+
}
|
|
132
715
|
```
|
|
133
|
-
> /mode # Show current mode
|
|
134
|
-
Current permission mode: default
|
|
135
716
|
|
|
136
|
-
|
|
137
|
-
|
|
717
|
+
`base` is any built-in and `overrides` is a sparse map over the same token paths the built-ins use —
|
|
718
|
+
`colors`, `markdown`, `syntax` and `motion` — so you change the colours you care about and inherit
|
|
719
|
+
the rest. Values use Ink's colour grammar: a chalk colour name, `#rgb`, `#rrggbb`, `ansi256(n)` or
|
|
720
|
+
`rgb(r,g,b)`. A raw escape sequence is not in that grammar, so it cannot enter through a theme.
|
|
721
|
+
|
|
722
|
+
A plugin ships themes the same way, in its own `themes/` directory; they are listed as
|
|
723
|
+
`custom:<plugin>:<file-name>`, and the plugin's own name has to satisfy the same rule for the same
|
|
724
|
+
reason. Both namespaces start with `custom:`, so a file can never take a
|
|
725
|
+
built-in's name whatever it is called.
|
|
726
|
+
|
|
727
|
+
A file is applied whole or not at all. An unknown token, a value that is not a colour, or JSON that
|
|
728
|
+
does not parse skips the WHOLE file with the path that refused it — printed once at startup as
|
|
729
|
+
`Skipped theme "mine.json": $.overrides.colors.text.accent: "nope" is not a colour …`, and shown in
|
|
730
|
+
the picker as a row that carries the same reason and cannot be chosen. Its neighbours still load.
|
|
731
|
+
|
|
732
|
+
Motion can also be decided per run: `--reduced-motion` / `--no-reduced-motion` beat
|
|
733
|
+
`ROBOTA_REDUCED_MOTION=1|0`, which beats the setting. A run that pins it says so — `/theme motion on`
|
|
734
|
+
reports that it saved the setting and that this run keeps what pinned it, rather than appearing to
|
|
735
|
+
do nothing. `NO_COLOR`, `FORCE_COLOR=0`, a non-TTY stdout and screen-reader mode still win over
|
|
736
|
+
every theme: no colour and no animation, exactly as before.
|
|
737
|
+
|
|
738
|
+
## Permission System
|
|
739
|
+
|
|
740
|
+
Every tool call passes through a three-step permission gate:
|
|
741
|
+
|
|
742
|
+
1. **Deny list** — if any deny pattern matches, the action is blocked
|
|
743
|
+
2. **Allow list** — if any allow pattern matches, the action is auto-approved
|
|
744
|
+
3. **Mode policy** — the active permission mode determines the decision
|
|
745
|
+
|
|
746
|
+
### Permission Modes
|
|
747
|
+
|
|
748
|
+
| Mode | Read/Glob/Grep | Write/Edit | Bash |
|
|
749
|
+
| ------------------- | :------------: | :--------: | :-----: |
|
|
750
|
+
| `plan` | auto | deny | deny |
|
|
751
|
+
| `default` | auto | approve | approve |
|
|
752
|
+
| `acceptEdits` | auto | auto | approve |
|
|
753
|
+
| `bypassPermissions` | auto | auto | auto |
|
|
754
|
+
|
|
755
|
+
### Changing Mode at Runtime
|
|
756
|
+
|
|
757
|
+
Use the `/permissions` slash command:
|
|
138
758
|
|
|
139
|
-
|
|
140
|
-
|
|
759
|
+
```
|
|
760
|
+
> /permissions # Show mode, rules by settings file, approvals, recent denials
|
|
761
|
+
> /permissions plan # Switch to plan (read-only)
|
|
762
|
+
> /permissions bypassPermissions # Skip all prompts
|
|
141
763
|
```
|
|
142
764
|
|
|
143
765
|
Or set it at startup:
|
|
@@ -146,7 +768,7 @@ Or set it at startup:
|
|
|
146
768
|
robota --permission-mode plan
|
|
147
769
|
```
|
|
148
770
|
|
|
149
|
-
### Permission Patterns
|
|
771
|
+
### Permission Patterns
|
|
150
772
|
|
|
151
773
|
Configure in `.robota/settings.json` or `.robota/settings.local.json`:
|
|
152
774
|
|
|
@@ -159,42 +781,360 @@ Configure in `.robota/settings.json` or `.robota/settings.local.json`:
|
|
|
159
781
|
}
|
|
160
782
|
```
|
|
161
783
|
|
|
162
|
-
|
|
784
|
+
Pattern syntax: `ToolName` matches any invocation; `ToolName(pattern)` matches on the primary argument with shell-style globs (`*`, `**`).
|
|
785
|
+
|
|
786
|
+
## Keyboard Controls
|
|
787
|
+
|
|
788
|
+
| Key | Action |
|
|
789
|
+
| ---------- | ----------------------------------------------------------- |
|
|
790
|
+
| Enter | Submit input |
|
|
791
|
+
| Ctrl+R | Search prompt history (see "Prompt history") |
|
|
792
|
+
| ESC | Abort current execution (graceful — saves partial response) |
|
|
793
|
+
| Ctrl+C | Exit process immediately |
|
|
794
|
+
| Up/Down | Navigate visual lines in wrapped multi-line input |
|
|
795
|
+
| Arrow keys | Navigate slash command autocomplete, permission prompt |
|
|
796
|
+
|
|
797
|
+
## Paste Handling
|
|
798
|
+
|
|
799
|
+
Bracketed paste mode (DECSET 2004) is enabled on startup. When pasting multiline text, the input area collapses it into a label: `[Pasted text #1 +42 lines]`. Multiple pastes are numbered sequentially. The full content is expanded on submit.
|
|
800
|
+
|
|
801
|
+
Single-line paste is inserted directly as typed text. Terminals without bracketed paste fall back to heuristic detection.
|
|
802
|
+
|
|
803
|
+
## Edit Diff Display
|
|
804
|
+
|
|
805
|
+
After the Edit tool runs, a `DiffBlock` component renders the change inline:
|
|
806
|
+
|
|
807
|
+
```
|
|
808
|
+
✓ Edit(src/provider.ts)
|
|
809
|
+
│ src/provider.ts
|
|
810
|
+
│ - const DEFAULT_MAX_TOKENS = 4096;
|
|
811
|
+
│ + const maxTokens = getModelMaxOutput(modelId);
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
Removed lines appear in red with `-`, added lines in green with `+`. Diffs longer than 10 lines show the first 8 + a `... and N more lines` summary.
|
|
815
|
+
|
|
816
|
+
## Session Management
|
|
817
|
+
|
|
818
|
+
The CLI supports continuing, resuming, forking, and naming sessions.
|
|
163
819
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
820
|
+
### CLI Flags
|
|
821
|
+
|
|
822
|
+
| Flag | Description |
|
|
823
|
+
| --------------------- | ------------------------------------------------ |
|
|
824
|
+
| `-c`, `--continue` | Continue the most recent session |
|
|
825
|
+
| `-r`, `--resume <id>` | Resume a specific session by ID |
|
|
826
|
+
| `--fork-session <id>` | Fork a session (new session with copied history) |
|
|
827
|
+
| `--name <name>` | Assign a name to the session at startup |
|
|
828
|
+
|
|
829
|
+
### TUI Commands
|
|
830
|
+
|
|
831
|
+
| Command | Description |
|
|
832
|
+
| ---------------- | ----------------------------------- |
|
|
833
|
+
| `/resume` | List recent sessions and resume one |
|
|
834
|
+
| `/rename <name>` | Rename the current session |
|
|
835
|
+
|
|
836
|
+
### Moving to another directory (`/cd`)
|
|
837
|
+
|
|
838
|
+
`/cd <directory>` continues the conversation in another directory. Robota starts again there, as if
|
|
839
|
+
launched in that directory: its settings, trust decision, tools, skills and `AGENTS.md` apply, and the
|
|
840
|
+
conversation resumes. The system prompt is kept as it was, so a provider's prompt cache survives. One
|
|
841
|
+
message tells the model about the new directory and its project instructions.
|
|
842
|
+
|
|
843
|
+
- `/cd` is refused while a turn is running or a background task is still running, and in a session
|
|
844
|
+
started with `--no-session-persistence`, which has no saved conversation to carry.
|
|
845
|
+
- A restricted (untrusted) session stays restricted after a move. A trusted session takes the target
|
|
846
|
+
directory's own trust decision.
|
|
847
|
+
- A `Cd(...)` deny rule keeps sessions out of a directory, for example
|
|
848
|
+
`"deny": ["Cd(/secrets/**)"]`.
|
|
849
|
+
|
|
850
|
+
### Session Name Display
|
|
851
|
+
|
|
852
|
+
When a session has a name, it appears in three places:
|
|
853
|
+
|
|
854
|
+
- **Input border** — session name shown in the input area border
|
|
855
|
+
- **Terminal title** — updated via ANSI escape sequences
|
|
856
|
+
- **StatusBar** — displayed alongside activity, model, and context usage
|
|
857
|
+
|
|
858
|
+
## Deep Links
|
|
859
|
+
|
|
860
|
+
A `robota://open` link starts a session in a directory you have already trusted, with a prompt
|
|
861
|
+
already in the composer and **not** submitted — you read it and press Enter, or clear it.
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
robota open 'robota://open?v=1&prompt=Summarize%20the%20README&cwd=/absolute/path/to/repo'
|
|
865
|
+
```
|
|
168
866
|
|
|
169
|
-
|
|
867
|
+
| Key | Meaning |
|
|
868
|
+
| -------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
869
|
+
| `v` | Contract version. Required, and must be `1`. |
|
|
870
|
+
| `prompt` | The text to prefill. At most 5,000 characters; it may not begin with `/`. |
|
|
871
|
+
| `cwd` | Absolute path of the directory to open. |
|
|
872
|
+
| `repo` | `owner/name` of an already-trusted local clone, when you do not want to name a path. `cwd` wins if both are present. |
|
|
873
|
+
|
|
874
|
+
Everything else is refused, and a refusal discards the whole link, says which rule it broke, writes
|
|
875
|
+
to stderr and exits non-zero without starting a session: an unknown key (so a link cannot carry
|
|
876
|
+
`provider=`, `permission-mode=`, `plugin=` or any other configuration), a duplicate key, a missing
|
|
877
|
+
or different `v`, a link over 8,192 characters, a prompt over 5,000, a prompt beginning with `/`
|
|
878
|
+
(one Enter would otherwise run it as a command), a relative, UNC or `..`-bearing path, and a second
|
|
879
|
+
link appended after the first (the argv shape a desktop handler can be made to produce). Your own
|
|
880
|
+
flags still apply after the link; a trailing token that is neither a link nor a flag is currently
|
|
881
|
+
discarded rather than refused — a CLI-wide gap tracked separately, not specific to links.
|
|
882
|
+
|
|
883
|
+
The target must already be trusted — `robota trust --yes` in that directory — for `cwd=` exactly as
|
|
884
|
+
for `repo=`. A link opens only what you have already approved; it never clones, never fetches, and
|
|
885
|
+
never reads a repository you have not trusted. While the composer still holds exactly what the link
|
|
886
|
+
supplied, the line `Prompt from an external link` sits below the input, and above 1,000 characters it
|
|
887
|
+
adds the character count and asks you to read the whole thing before sending. Edit that text and the
|
|
888
|
+
line goes: what is in the composer is then yours, and the label would be claiming otherwise.
|
|
889
|
+
|
|
890
|
+
**Known limitations.** No URL scheme is registered with the operating system yet, so a browser
|
|
891
|
+
cannot hand the link over: pass it to `robota open` yourself, or point your own handler at that
|
|
892
|
+
command. Registering the scheme on macOS, Linux and Windows, and the HTTPS launcher that works
|
|
893
|
+
around chat clients stripping custom schemes, are tracked separately.
|
|
170
894
|
|
|
171
895
|
## Slash Commands
|
|
172
896
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
| `/
|
|
180
|
-
| `/
|
|
181
|
-
| `/
|
|
897
|
+
Typing `/` in the TUI opens an autocomplete popup. Arrow keys navigate, Tab inserts without executing, Enter executes. Subcommands (e.g., `/provider list`) show a nested submenu.
|
|
898
|
+
|
|
899
|
+
### Session & Context
|
|
900
|
+
|
|
901
|
+
| Command | Description |
|
|
902
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
903
|
+
| `/clear` | Clear conversation history |
|
|
904
|
+
| `/compact [instructions]` | Compress context window |
|
|
905
|
+
| `/context` | Context window details, reference inventory, and auto-compact controls |
|
|
906
|
+
| `/cost` | Show session token usage and cost |
|
|
907
|
+
| `/effort [level]` | Show or change model effort (`auto`, `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) |
|
|
908
|
+
| `/resume` | List recent sessions and resume one |
|
|
909
|
+
| `/rename <name>` | Rename the current session |
|
|
910
|
+
| `/cd <directory>` | Move this conversation to another directory (see below) |
|
|
911
|
+
| `/rewind` | List, inspect, restore, or rollback edit checkpoints |
|
|
912
|
+
|
|
913
|
+
### Providers & Settings
|
|
914
|
+
|
|
915
|
+
| Command | Description |
|
|
916
|
+
| ------------------------ | -------------------------------------------------------------------- | ------- | -------------------------------------------------- |
|
|
917
|
+
| `/provider [subcommand]` | Manage provider profiles: `list`, `switch`, `add`, `test`, `current` |
|
|
918
|
+
| `/mode [mode]` | Show or switch permission mode |
|
|
919
|
+
| `/permissions [mode]` | Show permission rules or change permission mode |
|
|
920
|
+
| `/sandbox [mode]` | Show or change how shell commands are confined |
|
|
921
|
+
| `/settings` | Open transport settings (enable/disable transports) |
|
|
922
|
+
| `/language [lang]` | Set response language (ko, en, ja, zh), saves and restarts |
|
|
923
|
+
| `/statusline [on | off | reset]` | Configure status-line fields (model, context, git) |
|
|
924
|
+
|
|
925
|
+
### Tools & Memory
|
|
926
|
+
|
|
927
|
+
| Command | Description |
|
|
928
|
+
| ---------------------- | -------------------------------------------------------------------- |
|
|
929
|
+
| `/memory [subcommand]` | Inspect, add, or review project memory entries |
|
|
930
|
+
| `/background` | List and control background tasks |
|
|
931
|
+
| `/goal <objective>` | Assign an autonomous goal pursued across turns (`status` / `cancel`) |
|
|
932
|
+
| `/agent` | Run and manage background subagent jobs |
|
|
933
|
+
| `/skills [name]` | List registered skills or activate one by name |
|
|
934
|
+
| `/plugin [subcommand]` | Plugin management |
|
|
935
|
+
|
|
936
|
+
### Git
|
|
937
|
+
|
|
938
|
+
| Command | Description |
|
|
939
|
+
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
940
|
+
| `/git status` | Branch plus the staged, unstaged and untracked paths |
|
|
941
|
+
| `/git diff [--staged \| <rev> \| <a>..<b>] [-- <path> ...]` | Unstaged diff, staged diff, or a diff against one or two revisions (each revision is verified first) |
|
|
942
|
+
| `/git commit [<subject>]` | Commit the staged changes after a confirmation listing the message and the staged files |
|
|
943
|
+
|
|
944
|
+
`/git commit` operates on the staged set only — no `-a`, no paths. The subject must follow the
|
|
945
|
+
Conventional Commits form `<type>[(scope)][!]: <description>`; the type list, the 72-character limit
|
|
946
|
+
and a trailing period are warnings, not refusals. With nothing staged it says so, with the unstaged and
|
|
947
|
+
untracked counts. Headless (`-p`) runs cancel the commit because no confirmation can be asked for.
|
|
948
|
+
Other git flags are not accepted — `/shell git ...` remains the way to run arbitrary git.
|
|
949
|
+
|
|
950
|
+
### Sessions on this host
|
|
951
|
+
|
|
952
|
+
| Command | Description |
|
|
953
|
+
| ------------------------------------ | ----------------------------------------- |
|
|
954
|
+
| `/peers` | List the other live sessions on this host |
|
|
955
|
+
| `/peers send <session-id> <message>` | Send a message to one of them |
|
|
956
|
+
|
|
957
|
+
See [Talking to another session](#talking-to-another-session) for the flow and what happens when the
|
|
958
|
+
other session is busy.
|
|
959
|
+
|
|
960
|
+
### Utility
|
|
961
|
+
|
|
962
|
+
| Command | Description |
|
|
963
|
+
| -------- | -------------------------------------------------- |
|
|
964
|
+
| `/help` | Show available commands |
|
|
965
|
+
| `/reset` | Delete user settings and return to first-run state |
|
|
966
|
+
| `/exit` | Exit CLI |
|
|
967
|
+
|
|
968
|
+
Skill commands from the CLI's ordered `.robota/skills/`, `.claude/skills/`, `.claude/commands/`, and
|
|
969
|
+
`.agents/skills/` roots appear alongside built-in commands.
|
|
970
|
+
|
|
971
|
+
## Talking to another session
|
|
972
|
+
|
|
973
|
+
Two `robota` sessions running on the same host, as the same user, can see and address each other.
|
|
974
|
+
Nothing crosses a machine boundary and nothing is configured — a session becomes discoverable by
|
|
975
|
+
being alive and stops being discoverable when it exits.
|
|
976
|
+
|
|
977
|
+
### Seeing who is there
|
|
978
|
+
|
|
979
|
+
```
|
|
980
|
+
/peers
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
With nothing else running:
|
|
984
|
+
|
|
985
|
+
```
|
|
986
|
+
No other live session is announced. Start a second session on this host, as this user,
|
|
987
|
+
and it appears here.
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
That is a sentence rather than an empty list on purpose: "no one is there" and "discovery is not
|
|
991
|
+
working" are different answers, and an empty list cannot tell you which one you got.
|
|
992
|
+
|
|
993
|
+
With a second session up:
|
|
994
|
+
|
|
995
|
+
```
|
|
996
|
+
Live sessions:
|
|
997
|
+
b8319b98-bb7b-486c-a499-cf1585b39e61 (this session)
|
|
998
|
+
97ffafe3-f770-4877-90a4-bd86df6be010
|
|
999
|
+
|
|
1000
|
+
Send to one: /peers send <session-id> <message>
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Sending
|
|
1004
|
+
|
|
1005
|
+
```
|
|
1006
|
+
/peers send 97ffafe3-f770-4877-90a4-bd86df6be010 rerun the failing suite
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
The message becomes a **turn** in the other session — the agent there answers it as if the operator
|
|
1010
|
+
had typed it. The sender is told which of four things happened, as a sentence:
|
|
1011
|
+
|
|
1012
|
+
| Outcome | What it means |
|
|
1013
|
+
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
1014
|
+
| `Delivered to <id>.` | It arrived and started. |
|
|
1015
|
+
| `<id> has the message; it is waiting behind work already running there.` | It arrived and is queued. Deliberately **not** reported as delivered. |
|
|
1016
|
+
| `<id> had already seen that message.` | A retry that the receiver recognised; it does not run twice. |
|
|
1017
|
+
| `Not delivered to <id>. <reason>` | Nothing ran. The reason names the target, e.g. that no such session is announced. |
|
|
1018
|
+
|
|
1019
|
+
The second row is the one worth knowing. Reporting a queued message as delivered would hide a wait
|
|
1020
|
+
the operator can otherwise see and act on.
|
|
1021
|
+
|
|
1022
|
+
### What the receiving operator sees
|
|
1023
|
+
|
|
1024
|
+
The message is attributed to the sender, not to whoever is sitting at the receiving terminal:
|
|
1025
|
+
|
|
1026
|
+
```
|
|
1027
|
+
peer:b8319b98-bb7b-486c-a499-cf1585b39e61:
|
|
1028
|
+
rerun the failing suite
|
|
1029
|
+
Robota:
|
|
1030
|
+
…
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
Your own turns still read `You:`. The name in the label is **derived from the peer's session id** —
|
|
1034
|
+
it is not a display name the sender chose, because a name the transcript's reader trusts must not be
|
|
1035
|
+
picked by the party being named. It is display attribution only: nothing anywhere uses it to decide
|
|
1036
|
+
what a turn is allowed to do.
|
|
1037
|
+
|
|
1038
|
+
### When the other session is busy
|
|
1039
|
+
|
|
1040
|
+
A message arriving mid-turn joins that session's existing pending queue rather than interrupting or
|
|
1041
|
+
opening a second one. Three things follow from how that queue works:
|
|
1042
|
+
|
|
1043
|
+
- **Consecutive messages from the same sender coalesce**, last one wins. That is right for a person
|
|
1044
|
+
retyping and wrong for a peer saying two separate things. The replaced one is not swallowed: it
|
|
1045
|
+
settles as refused with the reason `coalesced`, so the sender learns it never ran rather than
|
|
1046
|
+
assuming both did. Tracked as `PEER-003`.
|
|
1047
|
+
- **Messages from different senders do not coalesce** — they queue in arrival order.
|
|
1048
|
+
- **The queue holds 32.** Beyond that a message settles as refused with the reason `dropped`.
|
|
1049
|
+
- **A cleared queue** — abort, cancel, or shutdown — settles the waiting entries with `cancelled`.
|
|
1050
|
+
|
|
1051
|
+
In every case the submission that never became a turn says so. A message that arrived and was then
|
|
1052
|
+
displaced is a different thing from one that ran, and the sender is told which it got.
|
|
1053
|
+
|
|
1054
|
+
### Limits
|
|
1055
|
+
|
|
1056
|
+
- Same host, same user. There is no network path here.
|
|
1057
|
+
- A session that exits removes its own entry; a crashed one is reaped by the next session that looks.
|
|
1058
|
+
- Session ids are what you address. There are no aliases.
|
|
1059
|
+
|
|
1060
|
+
## Plugin Management
|
|
1061
|
+
|
|
1062
|
+
The `/plugin` command opens an interactive TUI or runs plugin operations through the injected plugin command module:
|
|
1063
|
+
|
|
1064
|
+
| Subcommand | Description |
|
|
1065
|
+
| ---------------------------------------- | ------------------------------------- |
|
|
1066
|
+
| `/plugin` or `/plugin manage` | Open the plugin manager TUI |
|
|
1067
|
+
| `/plugin install <name>@<marketplace>` | Install a plugin from a marketplace |
|
|
1068
|
+
| `/plugin uninstall <name>@<marketplace>` | Remove an installed plugin |
|
|
1069
|
+
| `/plugin enable <name>@<marketplace>` | Enable a disabled plugin |
|
|
1070
|
+
| `/plugin disable <name>@<marketplace>` | Disable a plugin without uninstalling |
|
|
1071
|
+
| `/plugin marketplace add <source>` | Add a marketplace source |
|
|
1072
|
+
| `/plugin marketplace remove <name>` | Remove a marketplace source |
|
|
1073
|
+
| `/plugin marketplace update <name>` | Update a marketplace source |
|
|
1074
|
+
| `/plugin marketplace list` | List configured marketplace sources |
|
|
182
1075
|
|
|
183
1076
|
## Configuration
|
|
184
1077
|
|
|
185
|
-
Settings are
|
|
1078
|
+
Settings are merged in this order, from lowest to highest priority:
|
|
1079
|
+
|
|
1080
|
+
1. `~/.robota/settings.json` (user global)
|
|
1081
|
+
2. `~/.claude/settings.json` (user global, Claude Code compatible)
|
|
1082
|
+
3. `.robota/settings.json` (project, shared)
|
|
1083
|
+
4. `.robota/settings.local.json` (local, gitignored)
|
|
1084
|
+
5. `.claude/settings.json` (project, Claude Code compatible)
|
|
1085
|
+
6. `.claude/settings.local.json` (local, gitignored, Claude Code compatible)
|
|
1086
|
+
|
|
1087
|
+
The two user layers are always host-owned. The four project layers participate only when the CLI host
|
|
1088
|
+
supplies trusted project access; Restricted composition does not probe them. Project writes require a
|
|
1089
|
+
separately approved settings writer for the same authority.
|
|
1090
|
+
|
|
1091
|
+
### Workspace trust
|
|
1092
|
+
|
|
1093
|
+
Robota admits project-controlled settings and executable contributions only after a host-owned grant
|
|
1094
|
+
for the canonical Git workspace identity. In a new or revoked workspace, interactive startup remains
|
|
1095
|
+
usable with project settings, hooks, plugins, skills, and provider overrides disabled. Headless startup
|
|
1096
|
+
fails closed until trust is granted:
|
|
1097
|
+
|
|
1098
|
+
```bash
|
|
1099
|
+
robota trust status
|
|
1100
|
+
robota trust --yes
|
|
1101
|
+
robota trust revoke --yes
|
|
1102
|
+
```
|
|
186
1103
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
1104
|
+
The grant survives process restart and is invalidated by repository replacement, revocation, or a
|
|
1105
|
+
trust-store error. Symlink aliases resolve to the canonical workspace; a different repository at the
|
|
1106
|
+
same textual path does not inherit the grant. `robota doctor` reports trust and endpoint provenance
|
|
1107
|
+
without printing credentials. If a lower-trust settings layer changes a provider endpoint without
|
|
1108
|
+
providing its own key, Robota removes the inherited key and reports `provider endpoint quarantined`.
|
|
190
1109
|
|
|
191
1110
|
```json
|
|
192
1111
|
{
|
|
193
1112
|
"defaultMode": "default",
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
1113
|
+
"language": "en",
|
|
1114
|
+
"currentProvider": "qwen-plus",
|
|
1115
|
+
"providers": {
|
|
1116
|
+
"qwen-plus": {
|
|
1117
|
+
"type": "qwen",
|
|
1118
|
+
"model": "qwen-plus",
|
|
1119
|
+
"apiKey": "$ENV:DASHSCOPE_API_KEY",
|
|
1120
|
+
"baseURL": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
1121
|
+
},
|
|
1122
|
+
"supergemma4-26b-uncensored-v2": {
|
|
1123
|
+
"type": "gemma",
|
|
1124
|
+
"model": "supergemma4-26b-uncensored-v2",
|
|
1125
|
+
"apiKey": "lm-studio",
|
|
1126
|
+
"baseURL": "http://localhost:1234/v1"
|
|
1127
|
+
},
|
|
1128
|
+
"gpt-4o": {
|
|
1129
|
+
"type": "openai",
|
|
1130
|
+
"model": "gpt-4o",
|
|
1131
|
+
"apiKey": "$ENV:OPENAI_API_KEY"
|
|
1132
|
+
},
|
|
1133
|
+
"claude-sonnet-4-6": {
|
|
1134
|
+
"type": "anthropic",
|
|
1135
|
+
"model": "claude-sonnet-4-6",
|
|
1136
|
+
"apiKey": "$ENV:ANTHROPIC_API_KEY"
|
|
1137
|
+
}
|
|
198
1138
|
},
|
|
199
1139
|
"permissions": {
|
|
200
1140
|
"allow": ["Bash(pnpm *)"],
|
|
@@ -203,52 +1143,101 @@ Settings are loaded from (highest priority first):
|
|
|
203
1143
|
}
|
|
204
1144
|
```
|
|
205
1145
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1146
|
+
`currentProvider` selects a profile key from `providers`. The key is the stable profile identity, not
|
|
1147
|
+
the provider type; multiple profile keys may use the same provider type and model when they represent
|
|
1148
|
+
different credentials, endpoints, accounts, or operational defaults. Qwen Model Studio profiles use
|
|
1149
|
+
`type: "qwen"` with a DashScope-compatible `baseURL`; the API key is usually stored as
|
|
1150
|
+
`$ENV:DASHSCOPE_API_KEY`. DeepSeek profiles use `type: "deepseek"` with
|
|
1151
|
+
`https://api.deepseek.com` and `$ENV:DEEPSEEK_API_KEY`. Gemma-family LM Studio models use
|
|
1152
|
+
`type: "gemma"` so Robota can apply Gemma-specific channel-marker projection while still talking to
|
|
1153
|
+
the OpenAI-compatible `/v1/chat/completions` API through `baseURL`. Generic OpenAI-compatible profiles use
|
|
1154
|
+
`type: "openai"` and do not apply provider-specific projection. Use `--provider <profile>` for a
|
|
1155
|
+
one-shot invocation override; add `--set-current` only when the selected profile should become the
|
|
1156
|
+
persisted default. The legacy single-provider shape remains supported:
|
|
211
1157
|
|
|
212
|
-
|
|
1158
|
+
```json
|
|
1159
|
+
{
|
|
1160
|
+
"provider": {
|
|
1161
|
+
"name": "anthropic",
|
|
1162
|
+
"model": "claude-sonnet-4-6",
|
|
1163
|
+
"apiKey": "$ENV:ANTHROPIC_API_KEY"
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
213
1167
|
|
|
214
1168
|
## Context Discovery
|
|
215
1169
|
|
|
216
|
-
|
|
1170
|
+
With trusted project access, the CLI discovers and loads:
|
|
217
1171
|
|
|
218
|
-
- **AGENTS.md** — walking
|
|
219
|
-
- **CLAUDE.md** — same
|
|
1172
|
+
- **AGENTS.md** — walking only within the authenticated worktree root
|
|
1173
|
+
- **CLAUDE.md** — the same root-bounded discovery
|
|
220
1174
|
- **Project metadata** — from `package.json`, `tsconfig.json`
|
|
221
1175
|
|
|
222
|
-
All context is assembled into the system prompt
|
|
1176
|
+
All context is assembled into the system prompt.
|
|
1177
|
+
|
|
1178
|
+
Ordinary prompts may also reference workspace-local files with path-like `@file` tokens, for
|
|
1179
|
+
example `@AGENTS.md` or `@docs/SPEC.md`. The CLI passes those prompts through unchanged; the SDK
|
|
1180
|
+
resolves bounded file content through the accepted project reader, sends the enriched prompt to the model, and
|
|
1181
|
+
records a structured file-reference event in the session history.
|
|
1182
|
+
|
|
1183
|
+
## Memory Management
|
|
1184
|
+
|
|
1185
|
+
- **Message windowing** — React state keeps the most recent 100 messages. Older messages are dropped from the render tree; full history remains in the session store.
|
|
1186
|
+
- **Tool state cleanup** — Completed tool execution states are trimmed to the most recent 50 entries.
|
|
1187
|
+
- **React.memo** — `MessageItem` uses `React.memo` to skip redundant re-renders.
|
|
1188
|
+
|
|
1189
|
+
## Session Logging
|
|
1190
|
+
|
|
1191
|
+
Trusted composition writes project session logs and resumable records through authority-backed
|
|
1192
|
+
`session-logs` and `sessions` state facets. Restricted composition opens no project log path and uses
|
|
1193
|
+
the user session store instead. Session records include messages, UI history, the exact system prompt,
|
|
1194
|
+
registered tool schemas, and background task snapshots; high-frequency streaming chunks remain in
|
|
1195
|
+
the separately injected JSONL sink.
|
|
223
1196
|
|
|
224
1197
|
## Architecture
|
|
225
1198
|
|
|
1199
|
+
The CLI is a pure TUI layer. All business logic lives in `@robota-sdk/agent-framework`'s `InteractiveSession`. `useInteractiveSession` is the sole React↔SDK bridge, converting SDK events to React state.
|
|
1200
|
+
|
|
226
1201
|
```
|
|
227
|
-
bin.ts → cli.ts (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
1202
|
+
bin.ts → cli.ts (arg parsing)
|
|
1203
|
+
└── ui/render.tsx → App.tsx (thin JSX shell)
|
|
1204
|
+
├── useInteractiveSession (ONLY React↔SDK bridge)
|
|
1205
|
+
│ ├── InteractiveSession (SDK)
|
|
1206
|
+
│ ├── CommandRegistry (SDK, re-exported by CLI)
|
|
1207
|
+
│ │ ├── BuiltinCommandSource (SDK, empty by default)
|
|
1208
|
+
│ │ ├── agent-command (./skills) (/skills command + virtual skill aliases)
|
|
1209
|
+
│ │ ├── PluginCommandSource (SDK, plugin skills)
|
|
1210
|
+
│ │ └── ICommandModule sources (/help, /compact, ...)
|
|
1211
|
+
│ └── SystemCommandExecutor (SDK)
|
|
1212
|
+
├── plugin-hooks-merger.ts (merges plugin hooks into SDK config)
|
|
1213
|
+
├── MessageList.tsx
|
|
1214
|
+
├── InputArea.tsx (CjkTextInput, bracketed paste, slash detection)
|
|
1215
|
+
├── StatusBar.tsx (activity, conditional mode, model, context %)
|
|
1216
|
+
├── PermissionPrompt.tsx (arrow-key Allow/Deny)
|
|
1217
|
+
├── SlashAutocomplete.tsx (command popup with scroll)
|
|
1218
|
+
├── DiffBlock.tsx (Edit tool diff display)
|
|
1219
|
+
├── MenuSelect.tsx (arrow-key menu, Plugin TUI)
|
|
1220
|
+
├── PluginTUI.tsx (plugin management screen stack)
|
|
1221
|
+
├── TextPrompt.tsx (text input for Plugin TUI)
|
|
1222
|
+
└── ConfirmPrompt.tsx (reusable yes/no prompt)
|
|
240
1223
|
```
|
|
241
1224
|
|
|
242
|
-
|
|
1225
|
+
## Dependencies
|
|
243
1226
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1227
|
+
| Package | Purpose |
|
|
1228
|
+
| ----------------------------- | ----------------------------------------------------------------------------- |
|
|
1229
|
+
| `@robota-sdk/agent-framework` | Session factory, query, config, context |
|
|
1230
|
+
| `@robota-sdk/agent-core` | Types (TPermissionMode, TToolArgs) |
|
|
1231
|
+
| `@robota-sdk/agent-framework` | Headless runner and registry for print mode (`-p`); terminal I/O is CLI-local |
|
|
1232
|
+
| `ink` 7, `react` 19.2+ | TUI rendering |
|
|
1233
|
+
| `chalk` | Terminal colors |
|
|
1234
|
+
| `marked`, `marked-terminal` | Markdown parsing and terminal rendering |
|
|
1235
|
+
| `string-width` | Unicode-aware string width (CJK support) |
|
|
1236
|
+
|
|
1237
|
+
## Documentation
|
|
1238
|
+
|
|
1239
|
+
See [docs/SPEC.md](./docs/SPEC.md) for the full specification, architecture details, and design decisions.
|
|
1240
|
+
|
|
1241
|
+
## License
|
|
1242
|
+
|
|
1243
|
+
Robota is dual-licensed under the [GNU AGPL-3.0](../../LICENSE) or a [commercial license](../../COMMERCIAL.md). See [LICENSING.md](../../LICENSING.md).
|