@ocis/myagent-cli 0.2.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.
Files changed (91) hide show
  1. package/README.md +357 -0
  2. package/dist/agent/context.d.ts +33 -0
  3. package/dist/agent/context.js +169 -0
  4. package/dist/agent/modes.d.ts +21 -0
  5. package/dist/agent/modes.js +84 -0
  6. package/dist/agent/prompt-builder.d.ts +9 -0
  7. package/dist/agent/prompt-builder.js +31 -0
  8. package/dist/agent/sessions.d.ts +38 -0
  9. package/dist/agent/sessions.js +130 -0
  10. package/dist/agent/todo.d.ts +18 -0
  11. package/dist/agent/todo.js +61 -0
  12. package/dist/agent/turn.d.ts +309 -0
  13. package/dist/agent/turn.js +1253 -0
  14. package/dist/approval/policy.d.ts +81 -0
  15. package/dist/approval/policy.js +157 -0
  16. package/dist/config.d.ts +49 -0
  17. package/dist/config.js +156 -0
  18. package/dist/git/status.d.ts +89 -0
  19. package/dist/git/status.js +226 -0
  20. package/dist/headless.d.ts +72 -0
  21. package/dist/headless.js +330 -0
  22. package/dist/index.d.ts +60 -0
  23. package/dist/index.js +511 -0
  24. package/dist/protocol/client.d.ts +123 -0
  25. package/dist/protocol/client.js +250 -0
  26. package/dist/protocol/sse-frames.d.ts +6 -0
  27. package/dist/protocol/sse-frames.js +75 -0
  28. package/dist/protocol/types.d.ts +200 -0
  29. package/dist/protocol/types.js +8 -0
  30. package/dist/runtime.d.ts +38 -0
  31. package/dist/runtime.js +166 -0
  32. package/dist/sanitize.d.ts +1 -0
  33. package/dist/sanitize.js +21 -0
  34. package/dist/skills/discovery.d.ts +24 -0
  35. package/dist/skills/discovery.js +109 -0
  36. package/dist/tools/binary.d.ts +2 -0
  37. package/dist/tools/binary.js +22 -0
  38. package/dist/tools/diff.d.ts +1 -0
  39. package/dist/tools/diff.js +49 -0
  40. package/dist/tools/find.d.ts +2 -0
  41. package/dist/tools/find.js +61 -0
  42. package/dist/tools/fs.d.ts +2 -0
  43. package/dist/tools/fs.js +276 -0
  44. package/dist/tools/glob.d.ts +6 -0
  45. package/dist/tools/glob.js +131 -0
  46. package/dist/tools/grep.d.ts +3 -0
  47. package/dist/tools/grep.js +228 -0
  48. package/dist/tools/paths.d.ts +27 -0
  49. package/dist/tools/paths.js +124 -0
  50. package/dist/tools/registry.d.ts +13 -0
  51. package/dist/tools/registry.js +38 -0
  52. package/dist/tools/shell.d.ts +2 -0
  53. package/dist/tools/shell.js +136 -0
  54. package/dist/tools/skills.d.ts +2 -0
  55. package/dist/tools/skills.js +36 -0
  56. package/dist/tools/todo.d.ts +2 -0
  57. package/dist/tools/todo.js +43 -0
  58. package/dist/tools/transfer.d.ts +2 -0
  59. package/dist/tools/transfer.js +145 -0
  60. package/dist/tools/truncate.d.ts +12 -0
  61. package/dist/tools/truncate.js +46 -0
  62. package/dist/tools/types.d.ts +85 -0
  63. package/dist/tools/types.js +63 -0
  64. package/dist/ui/app.d.ts +39 -0
  65. package/dist/ui/app.js +1061 -0
  66. package/dist/ui/colors.d.ts +100 -0
  67. package/dist/ui/colors.js +169 -0
  68. package/dist/ui/components.d.ts +267 -0
  69. package/dist/ui/components.js +811 -0
  70. package/dist/ui/diff.d.ts +37 -0
  71. package/dist/ui/diff.js +143 -0
  72. package/dist/ui/format.d.ts +28 -0
  73. package/dist/ui/format.js +76 -0
  74. package/dist/ui/help.d.ts +6 -0
  75. package/dist/ui/help.js +45 -0
  76. package/dist/ui/highlight.d.ts +20 -0
  77. package/dist/ui/highlight.js +210 -0
  78. package/dist/ui/logo.d.ts +24 -0
  79. package/dist/ui/logo.js +106 -0
  80. package/dist/ui/model-list.d.ts +10 -0
  81. package/dist/ui/model-list.js +33 -0
  82. package/dist/ui/quit-confirm.d.ts +8 -0
  83. package/dist/ui/quit-confirm.js +40 -0
  84. package/dist/ui/select-popup.d.ts +30 -0
  85. package/dist/ui/select-popup.js +54 -0
  86. package/dist/ui/theme.d.ts +4 -0
  87. package/dist/ui/theme.js +41 -0
  88. package/dist/ui/tool-view.d.ts +20 -0
  89. package/dist/ui/tool-view.js +326 -0
  90. package/package.json +44 -0
  91. package/skills/git-commit/SKILL.md +27 -0
package/README.md ADDED
@@ -0,0 +1,357 @@
1
+ # myagent CLI
2
+
3
+ A coding agent for **your local repository**, powered by a remote MyAgent.
4
+ The CLI is the hands and eyes; the agent's brain — model, conversation,
5
+ compaction, cost accounting — stays on your MyAgent server.
6
+ Not everything is code: **COWORK** mode turns the same agent into a general assistant
7
+ (questions, lookups, summarizing, organizing) with the full tool set still available —
8
+ see [Plan / Coding / Cowork modes](#plan--coding--cowork-modes).
9
+
10
+ ```
11
+ local repo (cwd) remote myagent
12
+ ┌───────────────────────────────┐ ┌────────────────────────────────┐
13
+ │ myagent CLI │ │ agent loop (pi-agent-core) │
14
+ │ TUI: transcript · sidebar │ SSE / REST │ model / prompt cache / retry │
15
+ │ context: AGENTS.md/CLAUDE.md │◄──────────────►│ history / auto-compact / guard │
16
+ │ client tools ─────────────────┼─ AG-UI run line│ steering / stop / usage │
17
+ │ session index (local) │ + tool results│ (optional) server web_fetch │
18
+ └───────────────────────────────┘ └────────────────────────────────┘
19
+ ```
20
+
21
+ - **No provider keys on the client.** Models are configured on the MyAgent Models page;
22
+ the CLI only holds an integration key (`iak_…`). When the integration is set to *Client
23
+ choice*, `/model` selects among the server's models per new session.
24
+ - **Every coding tool runs on your machine.** The CLI advertises `local_*` tools to the
25
+ remote agent; when the agent calls one, it executes locally and submits the result.
26
+ - **Agent management stays remote.** Sessions, history, auto-compaction, prompt caching and
27
+ usage/cost accounting all live on MyAgent and are surfaced in the TUI through the
28
+ integration API.
29
+ - **One protocol: the AG-UI run line.** Each turn is a single `POST /v1/runs` stream that
30
+ stays open through tool waits — client tool calls (`CUSTOM_TOOL_CALL`), the agent's
31
+ continuation, and release signals all arrive on it. Tool results go back as protocol-native
32
+ `role: "tool"` messages to `POST /v1/runs/:threadId/messages` (single-frame
33
+ `INPUT_ACCEPTED`/`INPUT_REJECTED` ack). The CLI and the server deploy in lockstep — the
34
+ CLI assumes the AG-UI line exists.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install -g @ocis/myagent-cli # global (Node ≥22.19)
40
+ bunx @ocis/myagent-cli # one-off, no install (Bun)
41
+ ```
42
+
43
+ The CLI runs on **Node.js ≥22.19** or **Bun ≥1.0** — it detects the runtime it was
44
+ launched with. The published bin uses a Node shebang, so `bun install -g` also runs
45
+ it under Node; use `bunx` (or `bun <path-to-bin>`) to run it on Bun. Configure
46
+ credentials with `myagent config`, or set `MYAGENT_BASE_URL` /
47
+ `MYAGENT_INTEGRATION_ID` / `MYAGENT_API_KEY`.
48
+
49
+ ## Plan / Coding / Cowork modes
50
+
51
+ | Mode | Behavior |
52
+ |------|----------|
53
+ | **PLAN** (orange) | Investigate and propose. The mutating tools (`local_write`, `local_edit`, `local_shell`) refuse at execution time with a model-readable error; everything else (including `local_update_todo`) keeps working. |
54
+ | **CODING** (green, default) | Software work: implement, verify, report. |
55
+ | **COWORK** (purple) | General tasks — questions, lookups, summarizing, organizing, drafting. The full tool set stays available (including `local_write`/`local_edit`/`local_shell`); the mode only changes the framing, not the permissions. |
56
+
57
+ Switch with `Tab` (plan → coding → cowork → plan), `/plan`, `/coding`, `/cowork`, or start
58
+ with `--plan` / `--coding` / `--cowork`.
59
+
60
+ Modes are designed to be prompt-cache-friendly:
61
+
62
+ - **The advertised tool set never changes** — all tools are always in the schema, so the
63
+ cached tool block survives a switch.
64
+ - **The system prompt never changes with the mode** — it statically defines all modes'
65
+ behavior and the announcement protocol.
66
+ - **The mode is announced per user message** — when it changes (or on the first message of
67
+ a session), the CLI prepends `[mode: plan]` / `[mode: coding]` / `[mode: cowork]` to that
68
+ message. It never enters the system prompt, so the cached conversation prefix stays intact.
69
+
70
+ ## Tools (all client-side)
71
+
72
+ | Tool | Purpose |
73
+ |------|---------|
74
+ | `local_read` | Read a text file with line numbers (`offset`/`limit`, binary/size guards) |
75
+ | `local_write` | Create/overwrite (or append) a file, creating parent directories |
76
+ | `local_edit` | Exact-string replacement (unique or `replace_all`; batch `edits`), returns a unified diff. Literal replacement — `$&` in the new text is safe. |
77
+ | `local_shell` | Shell command on your machine, rooted at the workspace; 120s default timeout; process-group kill; 50KB UTF-8-safe output cap |
78
+ | `local_grep` | Regex search (ripgrep when installed, pure-JS fallback), 100-match cap, skips build/VCS dirs |
79
+ | `local_find` | Glob file discovery (`*`, `?`, `**`, `{a,b}`), 500-result cap |
80
+ | `local_ls` | Directory listing (optional recursive + glob filter) |
81
+ | `load_local_skill` | Load a `SKILL.md` playbook from the machine |
82
+ | `local_update_todo` | Maintain the session TODO list (rendered in the right sidebar). Batched into the same assistant turn as other tool calls to avoid an extra model round-trip. |
83
+ | `local_transfer_files` | Move single files between your machine and the myagent workspace (`direction: get`/`put`, plus `list`). Uses the workspace API; `get` writes locally (ASK/plan-mode rules apply), `put` does not touch your machine. Content never enters the model transcript. |
84
+
85
+ Side-effecting tools honor the approval mode: **AUTO** (default) runs in-workspace work
86
+ unattended but still asks before touching paths outside the workspace, while **ASK**
87
+ confirms every mutation (and outside reads). `Shift+Tab` or `/edit` toggles it per session
88
+ (changeable mid-run, it takes effect immediately); the current approval state is always
89
+ visible in the status bar (AUTO is amber), and the active mode in the prompt's bottom
90
+ edge. `--ask` starts in ASK, `--auto` (or the config default) starts in AUTO;
91
+ `--allow-outside` pre-approves outside paths so they never prompt.
92
+
93
+ **Workspace transfers.** `local_transfer_files` moves single files between your machine and
94
+ the myagent server's shared workspace (the same files the web file browser/WebDAV expose):
95
+ `{"direction":"put","from":"notes.txt"}` uploads, `{"direction":"get","from":"reports/q3.md"}`
96
+ downloads, `{"direction":"list","path":"/"}` browses. Uploads are capped at ~37 MB (the
97
+ server's 50 MB body limit after base64); both directions overwrite. Chat attachments made in
98
+ the web UI are stored outside the workspace and are not reachable this way.
99
+
100
+ ### Approval prompts
101
+
102
+ An approval floats directly above the prompt input (the editor keeps focus —
103
+ you can keep typing) and is answered with **ctrl+y** (approve) or **ctrl+n**
104
+ (deny). The wait is bounded: after **10 minutes** the approval times out and
105
+ the agent receives an error result telling it to stop and wait for you — a
106
+ run never hangs on an unanswered prompt.
107
+
108
+ Approvals **serialize**: one card is shown at a time and the rest queue in
109
+ arrival order (a round's parallel calls, or a second tool round starting while
110
+ you decide, can each need one). Each waiting approval keeps its own deadline
111
+ and its own cancellation, so a release of one call never cancels another and
112
+ `/stop` cancels every one of them.
113
+
114
+ While you decide, the CLI watches the session's event channel (when the server
115
+ advertises `sessionEvents`) and reports when the channel itself drops and is
116
+ being reconnected. If the server releases the call while you decide (the
117
+ 30-minute lease expired, the sandbox restarted, or the run ended), the
118
+ approval is **cancelled** — the tool never runs, and the tool card in the
119
+ transcript says so. Stopping the run (Esc, Ctrl+C, `/stop`) cancels a pending
120
+ approval the same way. If the sandbox was paused while you were away, the next
121
+ request wakes it automatically.
122
+
123
+ ## Context files
124
+
125
+ Every turn ships a stable system block containing:
126
+
127
+ - **`AGENTS.override.md` → `AGENTS.md` → `CLAUDE.md`** discovered per directory from the
128
+ filesystem root down to the workspace (outermost first), plus the global
129
+ `~/.config/myagent/AGENTS.md` and `.myagent/AGENTS.md`. Disable with `--no-context-files`.
130
+ - Environment facts (workspace, OS + release + arch). Date and git branch are deliberately
131
+ omitted — the agent can query them with `local_shell` when needed.
132
+ - The local `<available_skills>` catalog.
133
+ - Both modes' instructions and the `[mode: x]` announcement protocol (static — the active
134
+ mode never changes this block). The TODO list lives in the `local_update_todo` tool
135
+ calls, not here.
136
+
137
+ The block is re-sent each turn because the integration API treats system messages as
138
+ per-request instructions — it is kept byte-stable across turns (no mode, todos, date or
139
+ branch) so the provider prompt cache survives.
140
+
141
+ ## TUI
142
+
143
+ ```
144
+ ┌──────────────────────────────────────────────┬──────────────────────┐
145
+ │ transcript │ session title │
146
+ │ messages · tool calls · thinking · diffs │ CONTEXT │
147
+ │ │ 84k/200k (42% used) │
148
+ │ │ ~$0.42 (27% cached) │
149
+ │ │ TODO (2/5) │
150
+ │ │ CHANGES (12) │
151
+ │ ╭──────────────────────────────────────╮ │ M src/a.ts +18 -4 │
152
+ │ │ > editor (multi-line, / commands…) │ │ │
153
+ │ ╰─ CODING · sonnet-4-6 · high ─────────╯ │ │
154
+ │ AUTO · working · ↑ ↓ $ ~/repo:main │ │
155
+ └──────────────────────────────────────────────┴──────────────────────┘
156
+ ```
157
+
158
+ The layout splits left/right first: the transcript, prompt and status bar live in the
159
+ left column, so they never affect the right panel, which runs the full height of the
160
+ terminal.
161
+
162
+ - **Sidebar** (Ctrl+P toggles; hidden below 100 columns; ~25% of the terminal width):
163
+ the session title, a `CONTEXT` section (shown once usage is reported: `used / window
164
+ (% used)` on the first content line, cost with the cached-token share below), a `TODO`
165
+ section (only once the todo tool has been used), and `CHANGES` from `git`. Each section
166
+ title is prefixed with a hue-colored accent bar (`▌`) and sections have breathing room
167
+ above and below; the panel has a slightly lighter background (from the terminal's OSC 11
168
+ reply). Scroll with the mouse wheel or `Alt+↑/↓`; click `… N more` / press `Ctrl+E` to
169
+ expand the file list (collapsed shows 10, expanded shows every changed file).
170
+ - **Diff viewer**: click a changed file in the sidebar. The viewer fills the screen — a
171
+ gray rounded frame inset from the edges (nothing behind it leaks through), with the file
172
+ title, position and view mode centered in the top edge. Only the diff content is shown
173
+ (`diff --git`, `index`, `---`/`+++` and `@@` headers are hidden); changed lines carry
174
+ pre/post line numbers, `↑/↓ PgUp/PgDn` or the mouse wheel scrolls, `t` toggles
175
+ inline/side-by-side (`i`/`p` pick a view), `Esc` closes, and opening another file
176
+ replaces the popup. The card refits the screen on terminal resize/zoom (the scroll
177
+ position is clamped to the new height).
178
+ - **Prompt**: the editor sits in a rounded, inset frame (no background of its own); its
179
+ bottom edge carries the active mode (`PLAN` orange / `CODING` green / `COWORK` purple — `Tab`
180
+ cycles, click the label for the mode picker) followed by the model and thinking level in
181
+ gray (`·`-separated, both clickable to open their pickers), and a `↓ N more`
182
+ indicator while the input is scrolled.
183
+ - **Status bar**: gray text on the terminal's own background, separated from the prompt by a
184
+ blank row; approval (`AUTO`/`ASK`, switchable mid-run with Shift+Tab or by clicking the
185
+ badge), run state, the current/last run's token totals and cost sit on the left, and
186
+ `path:branch` is flush right.
187
+ - **Transcript**: assistant markdown (gray prose, indented `•` bullets and tables), a
188
+ collapsed thinking block (Ctrl+T), and tool calls collapsed to their header — e.g.
189
+ `▸ ✓ read src/app.ts · lines 1–4` (click one, or Ctrl+O for all, to expand). Expanded
190
+ results render by kind: syntax-highlighted file reads, side-by-side diffs with line
191
+ numbers for edits, `$ command` plus the last 30 output lines for bash (older lines
192
+ collapse to `... skipped`, failures are red). Server-tool activity and notices follow.
193
+ - **Approvals** appear as an overlay (`y`/`n`).
194
+ - **Editor**: `/` slash-command autocomplete, `@` file references, Shift+Enter or
195
+ Alt+Enter for a newline. (`Tab` is reserved for the mode cycle.)
196
+
197
+ ### Keys
198
+
199
+ | Key | Action |
200
+ |-----|--------|
201
+ | `Tab` | Cycle PLAN/CODING/COWORK |
202
+ | `Shift+Tab` | Toggle AUTO/ASK approval |
203
+ | `Enter` | Send; while running: **steer** the message into the in-flight turn |
204
+ | `Ctrl+Y` / `Ctrl+N` | Answer a pending approval (approve / deny) |
205
+ | `Esc` | Force-stop the run (`POST /stop`) |
206
+ | `Ctrl+C` | Stop while running; while idle, press twice within 2s to quit (`/quit` exits directly) |
207
+ | `Ctrl+P` | Toggle sidebar |
208
+ | `Ctrl+E` | Expand/collapse the changed-files list |
209
+ | `Alt+↑` / `Alt+↓` | Scroll the sidebar (mouse wheel works too) |
210
+ | Click a file | Open its diff in a popup |
211
+ | `t` / `i` / `p` | In the diff popup: toggle / inline / side-by-side pair |
212
+ | `Ctrl+O` | Expand/collapse all tool output (a single tool call toggles on click) |
213
+ | `Ctrl+T` | Expand/collapse thinking |
214
+ | `Shift+Enter` / `Alt+Enter` | Newline |
215
+
216
+ ### Commands
217
+
218
+ `/help` `/new` `/session [all]` (`/sessions`, `/resume` — aliases) `/plan` `/coding` `/cowork` `/mode` `/edit` `/thinking` `/model` `/compact`
219
+ `/stop` `/reload` `/export` `/config` `/quit`
220
+
221
+ ## Sessions
222
+
223
+ The remote session is the source of truth. The CLI keeps a small local index
224
+ (`~/.config/myagent/sessions.json`) mapping working directories to thread ids and TODO
225
+ lists so `-c` continues the right conversation.
226
+
227
+ - `/new` — start a fresh session (the remote thread is created on the next prompt).
228
+ - `/session` — list **this folder's** sessions and switch. The remote API has no working-
229
+ directory concept, so the filter comes from the local index; a "Show all folders…" entry
230
+ (or `/session all`) reveals every session on the integration, annotated with its source
231
+ folder. Switching to a session from another folder warns that tools still run in the
232
+ current workspace.
233
+ - `/export` writes a markdown transcript; `/sessions` and `/resume` are aliases of `/session`.
234
+
235
+ - **Steering**: typing while a run is active queues the message into the server-side run. It
236
+ floats above the prompt until the agent actually injects it (`QUEUED_MESSAGE_DELIVERED`),
237
+ then moves into the transcript as a user message.
238
+ - **Stop**: `Esc` or `/stop` aborts the stream and calls the stop endpoint.
239
+ - **Recovery**: if the stream drops, the CLI reconciles against the session detail
240
+ endpoint. Pending **read-only** tool calls are re-executed safely; pending **mutating**
241
+ calls are failed with an explanatory result instead of silently re-applied.
242
+ - **Usage**: live `RUN_USAGE` events update the sidebar; on resume, persisted
243
+ `usage_stats` seed the panel.
244
+
245
+ ## Model selection
246
+
247
+ The model runs server-side and is shown on the prompt's bottom edge. The integration's Model
248
+ policy decides who picks it:
249
+
250
+ - **Client choice** (the default): the CLI fetches the selectable models from
251
+ `GET /v1/models` and `/model` offers them in a picker. The server default (the first entry)
252
+ is marked **default**; the model in use is marked **current** (current wins when it is also
253
+ the default). `--model <id>` preselects one for new sessions.
254
+ - **A forced model**: the endpoint returns exactly one entry, so there is nothing to choose.
255
+
256
+ A thread pins its model on its first run, so `/model` applies to the **next** new session
257
+ (`/new`); the running thread keeps its model. Once a session has run, `/thinking` offers the
258
+ levels reported for that session's model.
259
+
260
+ ## Thinking level
261
+
262
+ The thinking level is selectable per request — the same behavior as the web UI's per-message
263
+ selector: `/thinking` offers the levels detected for the model plus **Default** (provider
264
+ decides). `--thinking <level>` sets it for a run.
265
+
266
+ ## Credentials & config
267
+
268
+ Resolved in order — flags > env > config file:
269
+
270
+ - Flags: `--base-url`, `--integration-id`, `--api-key`
271
+ - Env: `MYAGENT_BASE_URL`, `MYAGENT_INTEGRATION_ID`, `MYAGENT_API_KEY`, `MYAGENT_APPROVAL`
272
+ - File: `$XDG_CONFIG_HOME/myagent/config.json` (0600), written by `myagent config`
273
+
274
+ `--base-url` / `MYAGENT_BASE_URL` also accept the combined **API Base URL** shown on the
275
+ Integrations page (`https://host/integrations/<id>`) — the integration id is derived from it.
276
+
277
+ Create an integration + API key on the MyAgent **Integrations** page. Recommended settings
278
+ for the CLI:
279
+
280
+ - **Tools**: leave empty (all coding tools are supplied by the CLI) or enable `web_fetch` for
281
+ server-side fetching.
282
+ - **Model**: any tool-capable model with a reasonable context window.
283
+
284
+ The CLI and the server deploy in lockstep — every documented integration-API feature is
285
+ assumed to exist (there is no capability handshake).
286
+
287
+ ## Non-interactive use
288
+
289
+ ```bash
290
+ myagent -p "summarise the failing tests" # text to stdout, tools to stderr
291
+ cat README.md | myagent run "explain this" # prompt from stdin
292
+ myagent -p --output-format json "..." # one JSON object: thread_id/text/usage
293
+ myagent -p --output-format stream-json "..." # JSONL events as they happen
294
+ ```
295
+
296
+ ### Prompt mode (CI)
297
+
298
+ `--prompt` is the unattended one-shot entry point for CI and scripts:
299
+
300
+ ```bash
301
+ myagent --prompt "fix the failing tests" # final answer to stdout
302
+ cat prompt.md | myagent --prompt - # prompt from stdin
303
+ myagent --prompt "..." --session <thread-id> # continue a previous session
304
+ myagent --prompt "..." --output-format json --timeout 600 # machine-readable, bounded
305
+ ```
306
+
307
+ - **Non-TUI** — `--prompt` always runs headless, even on a TTY.
308
+ - **AUTO accept all** — every tool runs unattended, *including paths outside the
309
+ workspace* (no one is present to answer an approval prompt). `--ask` cannot be
310
+ honored here and is ignored — passing both prints a warning on stderr rather
311
+ than silently dropping the safer setting. `--plan` still works (read-only).
312
+ - **stdout is only the final assistant message** (tool progress goes to stderr).
313
+ `--output-format json` emits one JSON object — `thread_id`, `model`, `text`,
314
+ `usage`, `error`, `error_code` — and it is emitted for pre-run failures too, so
315
+ a parser never sees an empty stdout.
316
+ - **`--timeout <sec>`** aborts the run (and stops it server-side) after N seconds.
317
+ - **SIGINT/SIGTERM** stop the remote run gracefully (exit 130/143); a second
318
+ signal exits immediately.
319
+ - **Continuation**: the JSON `thread_id` is the session id — pass it back as
320
+ `--session <thread-id>` to continue the same context on the next run. `-c`
321
+ continues the most recent session for the working directory (local index).
322
+
323
+ Exit codes: `0` success · `1` run failed (including timeout) · `2` usage,
324
+ configuration, or connection error · `130`/`143` interrupted.
325
+
326
+ For CI, configure credentials via env (`MYAGENT_BASE_URL` — the combined
327
+ `https://host/integrations/<id>` URL works — `MYAGENT_INTEGRATION_ID`,
328
+ `MYAGENT_API_KEY`). `--prompt` sends its own final-answer instruction, so the
329
+ agent returns only the final answer without any integration-side configuration.
330
+
331
+ `-p`/`run` remain the general headless mode: they respect the configured
332
+ approval default (and `--ask` denies mutating tools), and stream all assistant
333
+ text. `--prompt` is the opinionated CI mode on top of the same runner.
334
+
335
+ ## Local skills
336
+
337
+ A skill is a directory with `SKILL.md` (frontmatter `name`/`description` + instructions).
338
+ Search paths, earlier wins: `--skills-dir`, `<workspace>/.myagent/skills`,
339
+ `~/.config/myagent/skills`. The catalog is injected into the system block; the agent loads
340
+ one with `load_local_skill`.
341
+
342
+ ## Development
343
+
344
+ ```bash
345
+ bun install
346
+ bun run dev # run from source
347
+ bun test # unit + e2e (fake integration server) tests
348
+ bun run typecheck
349
+ bun run build # npm package → dist/ (tsc + fix-esm; prepack runs this)
350
+ bun run build:binary # standalone binary → dist/myagent
351
+ bun run build:all # cross-compile linux/macos/windows (x64 + arm64)
352
+ ```
353
+
354
+ `bun run build` emits the publishable ESM package to `dist/` (`prepack` runs it
355
+ automatically on `npm publish`). The compiled binary has no runtime dependencies.
356
+ `@earendil-works/pi-tui` provides the rendering layer (the same library the pi
357
+ coding agent ships with).
@@ -0,0 +1,33 @@
1
+ export interface ContextFile {
2
+ path: string;
3
+ content: string;
4
+ }
5
+ /**
6
+ * Discover context files: the global file first, then per-directory files
7
+ * from the trust boundary down to cwd (outermost first), so the most local
8
+ * instructions come last. One file per directory; AGENTS.override.md wins.
9
+ *
10
+ * The walk is bounded (see contextBoundary): an unbounded walk to `/` would
11
+ * let anyone who can write to a shared ancestor (e.g. /tmp/AGENTS.md) inject
12
+ * instructions into every turn's system prompt — and in auto-approval mode
13
+ * drive unsupervised tool calls.
14
+ */
15
+ export declare function findContextFiles(cwd: string, globalContextPath?: string): Promise<ContextFile[]>;
16
+ export interface EnvironmentInfo {
17
+ workspace: string;
18
+ /** OS descriptor, e.g. "linux 6.8.0 (x64)". */
19
+ os: string;
20
+ }
21
+ /** Stable OS descriptor for the environment block (no volatile values). */
22
+ export declare function osDescription(): string;
23
+ export declare function buildEnvironmentBlock(info: EnvironmentInfo): string;
24
+ export interface SystemPromptInput {
25
+ workspace: string;
26
+ contextFiles: ContextFile[];
27
+ environment: EnvironmentInfo;
28
+ skillsCatalog?: string;
29
+ /** Additional instructions (e.g. per-session user config). */
30
+ extraInstructions?: string;
31
+ }
32
+ /** Compose the per-request system block. Must be byte-stable across turns. */
33
+ export declare function composeSystemPrompt(input: SystemPromptInput): string;
@@ -0,0 +1,169 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Local context assembly for the remote agent.
3
+ //
4
+ // The agent's brain runs on MyAgent; the CLI is the only party that can see
5
+ // the user's repository. Every turn therefore ships a system block carrying:
6
+ // - context files (AGENTS.md / CLAUDE.md) discovered from cwd upward
7
+ // - environment facts (workspace, OS) — deliberately NOT date/git branch,
8
+ // which change during a session and would invalidate the provider prompt
9
+ // cache (the agent can query them with local_shell when needed)
10
+ // - the local skills catalog
11
+ // - every mode's instructions and the mode-announcement protocol
12
+ //
13
+ // The block is re-sent each turn because the integration API treats `system`
14
+ // messages as per-request instructions (replace semantics); it is therefore
15
+ // kept byte-stable across turns so the cached prefix survives. The current
16
+ // mode/todos never enter this block — the mode arrives as a `[mode: x]` marker
17
+ // in the user message, and todos live in the local_update_todo tool calls.
18
+ // ---------------------------------------------------------------------------
19
+ import { homedir, release } from "node:os";
20
+ import { readFile, stat } from "node:fs/promises";
21
+ import { dirname, join, resolve, sep } from "node:path";
22
+ import { DEFAULT_MODE, MODES } from "./modes.js";
23
+ const PER_DIR_CANDIDATES = [
24
+ "AGENTS.override.md",
25
+ "AGENTS.md",
26
+ "AGENTS.MD",
27
+ "CLAUDE.md",
28
+ "CLAUDE.MD",
29
+ ".myagent/AGENTS.md",
30
+ ];
31
+ async function readIfExists(path) {
32
+ try {
33
+ const info = await stat(path);
34
+ if (!info.isFile())
35
+ return null;
36
+ const text = await readFile(path, "utf-8");
37
+ return text.trim() ? text : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * The outermost directory whose context files are trusted: the git repository
45
+ * root when cwd is inside one, else $HOME when cwd is under it, else cwd.
46
+ */
47
+ async function contextBoundary(start) {
48
+ for (let dir = start;;) {
49
+ if (await pathExists(join(dir, ".git")))
50
+ return dir;
51
+ const parent = dirname(dir);
52
+ if (parent === dir)
53
+ break;
54
+ dir = parent;
55
+ }
56
+ const home = homedir();
57
+ return start === home || start.startsWith(home + sep) ? home : start;
58
+ }
59
+ async function pathExists(path) {
60
+ try {
61
+ await stat(path);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * Discover context files: the global file first, then per-directory files
70
+ * from the trust boundary down to cwd (outermost first), so the most local
71
+ * instructions come last. One file per directory; AGENTS.override.md wins.
72
+ *
73
+ * The walk is bounded (see contextBoundary): an unbounded walk to `/` would
74
+ * let anyone who can write to a shared ancestor (e.g. /tmp/AGENTS.md) inject
75
+ * instructions into every turn's system prompt — and in auto-approval mode
76
+ * drive unsupervised tool calls.
77
+ */
78
+ export async function findContextFiles(cwd, globalContextPath) {
79
+ const files = [];
80
+ if (globalContextPath) {
81
+ const content = await readIfExists(globalContextPath);
82
+ if (content)
83
+ files.push({ path: globalContextPath, content });
84
+ }
85
+ const start = resolve(cwd);
86
+ const boundary = await contextBoundary(start);
87
+ const ancestors = [];
88
+ for (let dir = start;;) {
89
+ ancestors.push(dir);
90
+ if (dir === boundary)
91
+ break;
92
+ const parent = dirname(dir);
93
+ if (parent === dir)
94
+ break; // defensive: the boundary is always an ancestor
95
+ dir = parent;
96
+ }
97
+ // boundary → cwd
98
+ ancestors.reverse();
99
+ for (const dir of ancestors) {
100
+ for (const candidate of PER_DIR_CANDIDATES) {
101
+ const full = join(dir, candidate);
102
+ const content = await readIfExists(full);
103
+ if (content) {
104
+ files.push({ path: full, content });
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ return files;
110
+ }
111
+ /** Stable OS descriptor for the environment block (no volatile values). */
112
+ export function osDescription() {
113
+ return `${process.platform} ${release()} (${process.arch})`;
114
+ }
115
+ export function buildEnvironmentBlock(info) {
116
+ return [
117
+ `<environment>`,
118
+ `workspace: ${info.workspace}`,
119
+ `os: ${info.os}`,
120
+ `</environment>`,
121
+ ].join("\n");
122
+ }
123
+ /**
124
+ * Static description of ALL modes plus the announcement protocol. Never
125
+ * changes with the active mode — the current mode is announced per user
126
+ * message as `[mode: plan]` / `[mode: coding]` / `[mode: cowork]`.
127
+ */
128
+ function buildModesBlock() {
129
+ const modes = Object.keys(MODES);
130
+ const markers = modes.map((mode) => `[mode: ${mode}]`);
131
+ const markerList = `${markers.slice(0, -1).join(", ")} or ${markers.at(-1)}`;
132
+ const sections = modes.map((mode) => [`<${mode}_mode>`, MODES[mode].instructions, `</${mode}_mode>`].join("\n"));
133
+ return [
134
+ "<modes>",
135
+ "You operate in one of the modes below. Each user message may start with a mode marker —",
136
+ `${markerList} — announcing which mode you must follow; the`,
137
+ "latest marker wins. If no marker is present, keep following the last announced mode",
138
+ `(${DEFAULT_MODE} initially). All modes share the same tools: in plan mode the mutating tools refuse`,
139
+ "with an error instead of executing, so do not call them.",
140
+ "",
141
+ sections.join("\n\n"),
142
+ "</modes>",
143
+ ].join("\n");
144
+ }
145
+ /** Compose the per-request system block. Must be byte-stable across turns. */
146
+ export function composeSystemPrompt(input) {
147
+ const parts = [];
148
+ parts.push([
149
+ "<cli_role>",
150
+ "You are MyAgent CLI, an assistant running on the user's machine.",
151
+ `All tools you are given execute locally inside the workspace: ${input.workspace}`,
152
+ "Always use those tools for file and shell work; there is no server-side copy of the files.",
153
+ "Paths are workspace-relative. The announced mode defines whether the task is software work",
154
+ "or general assistance — follow it.",
155
+ "</cli_role>",
156
+ ].join("\n"));
157
+ parts.push(buildModesBlock());
158
+ if (input.contextFiles.length > 0) {
159
+ const blocks = input.contextFiles.map((file) => `--- ${file.path} ---\n${file.content.trimEnd()}`);
160
+ parts.push(`<project_context>\n${blocks.join("\n\n")}\n</project_context>`);
161
+ }
162
+ parts.push(buildEnvironmentBlock(input.environment));
163
+ if (input.skillsCatalog)
164
+ parts.push(input.skillsCatalog);
165
+ if (input.extraInstructions?.trim()) {
166
+ parts.push(`<user_instructions>\n${input.extraInstructions.trim()}\n</user_instructions>`);
167
+ }
168
+ return parts.join("\n\n");
169
+ }
@@ -0,0 +1,21 @@
1
+ export type AgentMode = "plan" | "coding" | "cowork";
2
+ /** The mode a fresh session starts in. */
3
+ export declare const DEFAULT_MODE: AgentMode;
4
+ export interface ModeSpec {
5
+ name: AgentMode;
6
+ /** Uppercase badge for the prompt frame / footer. */
7
+ label: string;
8
+ /** One-line UI blurb (mode picker rows). */
9
+ description: string;
10
+ /** Instructions for this mode, rendered into the static <modes> prompt block. */
11
+ instructions: string;
12
+ /** Mode accent color for badge + editor border. */
13
+ color: (s: string) => string;
14
+ }
15
+ export declare const MODES: Record<AgentMode, ModeSpec>;
16
+ /** `[mode: plan]` / `[mode: coding]` / `[mode: cowork]` marker at the start of a user message. */
17
+ export declare function modeMarker(mode: AgentMode): string;
18
+ export declare function stripModeMarker(text: string): string;
19
+ /** Tab / `/mode` cycle: plan → coding → cowork → plan. */
20
+ export declare function nextMode(mode: AgentMode): AgentMode;
21
+ export declare function isReadOnlyTool(name: string): boolean;
@@ -0,0 +1,84 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Plan / Coding / Cowork modes.
3
+ //
4
+ // IMPORTANT: the advertised tool set is mode-independent (a stable tool schema
5
+ // block keeps the provider prompt cache warm across mode switches). A mode is
6
+ // enforced at EXECUTION time: in plan mode mutating tools refuse with an error,
7
+ // and the mode is announced to the model per user message via a `[mode: x]`
8
+ // marker (see `modeMarker`). The system prompt statically defines all modes'
9
+ // behavior — it never changes with the mode.
10
+ // ---------------------------------------------------------------------------
11
+ import { CODING_COLOR, COWORK_COLOR, PLAN_COLOR } from "../ui/colors.js";
12
+ /** The mode a fresh session starts in. */
13
+ export const DEFAULT_MODE = "coding";
14
+ /** Tools whose execution never changes the user's machine. */
15
+ const READ_ONLY_TOOLS = ["local_read", "local_grep", "local_find", "local_ls", "load_local_skill"];
16
+ export const MODES = {
17
+ plan: {
18
+ name: "plan",
19
+ label: "PLAN",
20
+ color: PLAN_COLOR,
21
+ description: "read-only — investigate and propose",
22
+ instructions: [
23
+ "# Mode: PLAN (read-only)",
24
+ "You are in planning mode. Investigate and propose; do not change anything.",
25
+ "- Use the read-only tools to explore the codebase and gather evidence.",
26
+ "- Mutating tools (local_write, local_edit, local_shell) are advertised but REFUSE with an error in this mode — do not call them.",
27
+ "- local_update_todo is bookkeeping only and remains available if you want to lay out a plan checklist; batch it into the same turn as your other tool calls (parallel tool calling), never a turn of its own.",
28
+ "- Produce a concrete plan: files to change, the approach, risks, verification steps.",
29
+ "- Ask clarifying questions when the request is ambiguous instead of guessing.",
30
+ "- Keep the final plan concise and directly actionable — the user will switch to Coding mode to execute it.",
31
+ ].join("\n"),
32
+ },
33
+ coding: {
34
+ name: "coding",
35
+ label: "CODING",
36
+ color: CODING_COLOR,
37
+ description: "software work, end to end",
38
+ instructions: [
39
+ "# Mode: CODING (software engineering)",
40
+ "You are in coding mode. Carry out the software task end-to-end.",
41
+ "- Read before you write; prefer editing existing files over creating new ones.",
42
+ "- Keep changes minimal and consistent with the existing code.",
43
+ "- Use local_update_todo to track multi-step work (send the entire list each time). Batch it into the same turn as the tool calls it describes (parallel tool calling) — a todo-only turn wastes a model round-trip.",
44
+ "- Verify your changes (run tests/typecheck/build with local_shell where available).",
45
+ "- Keep the user informed with brief progress notes, then finish with a short summary.",
46
+ ].join("\n"),
47
+ },
48
+ cowork: {
49
+ name: "cowork",
50
+ label: "COWORK",
51
+ color: COWORK_COLOR,
52
+ description: "general tasks and everyday work",
53
+ instructions: [
54
+ "# Mode: COWORK (general tasks)",
55
+ "You are in cowork mode: a general-purpose assistant, not primarily a coding agent.",
56
+ "- Help with everyday work — answering questions, looking things up, summarizing, organizing information, drafting text, lightweight analysis.",
57
+ "- Do not assume the task involves a repository or code; ask when the goal is unclear.",
58
+ "- Answer directly and concisely first; expand only when it adds value.",
59
+ "- The full tool set is available (local_read, local_grep, local_find, local_ls, local_shell, local_write, local_edit, local_update_todo, local_transfer_files): use the tools when they genuinely help (inspect local files, run a command, verify a fact, move files to/from the myagent workspace). Do not refuse a tool just because the mode is cowork.",
60
+ "- local_update_todo remains available for multi-step work; batch it into the same turn as your other tool calls (parallel tool calling).",
61
+ "- Reply in the user's language.",
62
+ ].join("\n"),
63
+ },
64
+ };
65
+ /** `[mode: plan]` / `[mode: coding]` / `[mode: cowork]` marker at the start of a user message. */
66
+ export function modeMarker(mode) {
67
+ return `[mode: ${mode}]`;
68
+ }
69
+ /**
70
+ * Remove a leading mode marker (used when rendering server-side history).
71
+ * `build`/`chat` are the pre-rename ids — old sessions still carry them.
72
+ */
73
+ const MODE_MARKER_PATTERN = new RegExp(`^\\[mode: (?:${[...Object.keys(MODES), "build", "chat"].join("|")})\\]\\s*\\n*`);
74
+ export function stripModeMarker(text) {
75
+ return text.replace(MODE_MARKER_PATTERN, "");
76
+ }
77
+ /** Tab / `/mode` cycle: plan → coding → cowork → plan. */
78
+ export function nextMode(mode) {
79
+ const order = Object.keys(MODES);
80
+ return order[(order.indexOf(mode) + 1) % order.length];
81
+ }
82
+ export function isReadOnlyTool(name) {
83
+ return READ_ONLY_TOOLS.includes(name);
84
+ }