@iinm/plain-agent 1.11.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/iinm/plain-agent)
4
4
  [![npm version](https://img.shields.io/npm/v/@iinm/plain-agent)](https://www.npmjs.com/package/@iinm/plain-agent)
5
5
  [![install size](https://packagephobia.com/badge?p=@iinm/plain-agent)](https://packagephobia.com/result?p=@iinm/plain-agent)
6
- [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.11.8)](https://socket.dev/npm/package/@iinm/plain-agent)
6
+ [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.12.0)](https://socket.dev/npm/package/@iinm/plain-agent)
7
7
  [![CodeQL](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/iinm/plain-agent/actions/workflows/github-code-scanning/codeql)
8
8
 
9
9
  A lightweight terminal-based coding agent focused on safety and low token cost
@@ -25,7 +25,6 @@ A lightweight terminal-based coding agent focused on safety and low token cost
25
25
  - [Prompts](#prompts)
26
26
  - [Subagents](#subagents)
27
27
  - [Claude Code Plugin Support](#claude-code-plugin-support)
28
- - [Voice Input](#voice-input)
29
28
  - [Appendix: Creating Least-Privilege Users for Cloud Providers](#appendix-creating-least-privilege-users-for-cloud-providers)
30
29
  - [Developer Notes](#developer-notes)
31
30
 
@@ -545,16 +544,6 @@ plain resume --list
545
544
  plain resume 2026-05-10-0803-a7k
546
545
  ```
547
546
 
548
- Set up Plain Agent for your project.
549
-
550
- ```
551
- /configure Auto-approve file writes and patches
552
- ```
553
-
554
- ```
555
- /configure Set up a sandbox for this project
556
- ```
557
-
558
547
  ## Configuration
559
548
 
560
549
  Files are loaded in the following order. Settings in later files override earlier ones.
@@ -858,52 +847,6 @@ Example:
858
847
  plain install-claude-code-plugins
859
848
  ```
860
849
 
861
- ## Voice Input
862
-
863
- Press **Ctrl-O** to start recording, then press it again to stop. Partial transcripts are inserted into the prompt as you speak, so you can edit and send them like regular text.
864
-
865
- ### Requirements
866
-
867
- - A recording command on `PATH`: `arecord`, `sox`, or `ffmpeg`.
868
- - An API key for the chosen provider.
869
- - Your host must have microphone access.
870
-
871
- ### Providers
872
-
873
- **OpenAI Realtime**
874
-
875
- ```js
876
- // ~/.config/plain-agent/config.local.json
877
- {
878
- "voiceInput": {
879
- "provider": "openai",
880
- "apiKey": "<OPENAI_API_KEY>"
881
- // "model": "gpt-4o-transcribe", // or "gpt-4o-mini-transcribe", "whisper-1"
882
- // "language": "ja" // ISO-639-1 code. Improves accuracy and latency.
883
- }
884
- }
885
- ```
886
-
887
- **Gemini Live**
888
-
889
- ```js
890
- // ~/.config/plain-agent/config.local.json
891
- {
892
- "voiceInput": {
893
- "provider": "gemini",
894
- "apiKey": "<GEMINI_API_KEY>"
895
- // "model": "gemini-3.1-flash-live-preview",
896
- // "language": "ja"
897
- }
898
- }
899
- ```
900
-
901
- ### Options
902
-
903
- - `toggleKey` — Rebind the toggle key. Accepts `"ctrl-<char>"` where `<char>`
904
- is a letter (a-z) or one of `[ \ ] ^ _`. Defaults to `"ctrl-o"`.
905
- - `recorder` — Override automatic recorder detection, e.g. `{ "command": "sox", "args": ["-q", "-d", "-b", "16", "-c", "1", "-r", "24000", "-e", "signed-integer", "-t", "raw", "-"] }`. It must write raw 16-bit little-endian mono PCM to stdout at 24 kHz (OpenAI) or 16 kHz (Gemini).
906
-
907
850
  ## Appendix: Creating Least-Privilege Users for Cloud Providers
908
851
 
909
852
  <details>
@@ -5,9 +5,15 @@
5
5
  "patterns": [
6
6
  {
7
7
  "toolName": "exec_command",
8
- "input": { "command": { "$regex": "^(find|grep)$" } },
8
+ "input": { "command": "find" },
9
9
  "action": "deny",
10
- "reason": "Use rg or fd instead"
10
+ "reason": "Use fd instead; fd respects .gitignore by default"
11
+ },
12
+ {
13
+ "toolName": "exec_command",
14
+ "input": { "command": "grep" },
15
+ "action": "deny",
16
+ "reason": "Use rg instead; rg respects .gitignore by default"
11
17
  },
12
18
  {
13
19
  "toolName": "exec_command",
@@ -146,6 +152,16 @@
146
152
  }
147
153
  ],
148
154
  "tests": [
155
+ {
156
+ "desc": "find should be denied",
157
+ "toolUse": { "toolName": "exec_command", "input": { "command": "find" } },
158
+ "expectedAction": "deny"
159
+ },
160
+ {
161
+ "desc": "grep should be denied",
162
+ "toolUse": { "toolName": "exec_command", "input": { "command": "grep" } },
163
+ "expectedAction": "deny"
164
+ },
149
165
  {
150
166
  "desc": "ls should be allowed",
151
167
  "toolUse": { "toolName": "exec_command", "input": { "command": "ls" } },
@@ -1505,7 +1521,7 @@
1505
1521
  },
1506
1522
 
1507
1523
  {
1508
- "name": "glm-5.1",
1524
+ "name": "glm-5.2",
1509
1525
  "variant": "fireworks",
1510
1526
  "platform": {
1511
1527
  "name": "openai-compatible",
@@ -1514,7 +1530,7 @@
1514
1530
  "model": {
1515
1531
  "format": "openai-messages",
1516
1532
  "config": {
1517
- "model": "accounts/fireworks/models/glm-5p1"
1533
+ "model": "accounts/fireworks/models/glm-5p2"
1518
1534
  }
1519
1535
  },
1520
1536
  "cost": {
@@ -1528,7 +1544,7 @@
1528
1544
  }
1529
1545
  },
1530
1546
  {
1531
- "name": "glm-5.1",
1547
+ "name": "glm-5.2",
1532
1548
  "variant": "novita",
1533
1549
  "platform": {
1534
1550
  "name": "openai-compatible",
@@ -1537,7 +1553,7 @@
1537
1553
  "model": {
1538
1554
  "format": "openai-messages",
1539
1555
  "config": {
1540
- "model": "zai-org/glm-5.1"
1556
+ "model": "zai-org/glm-5.2"
1541
1557
  }
1542
1558
  },
1543
1559
  "cost": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.11.8",
3
+ "version": "1.12.0",
4
4
  "description": "A lightweight terminal-based coding agent focused on safety and low token cost",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -106,7 +106,7 @@ Examples:
106
106
  Preset Configuration:
107
107
 
108
108
  When --dockerfile is not specified, a preset Debian stable image is used with:
109
- - System packages: busybox, bash, zsh (with grml config), ripgrep, fd, dig, curl, git
109
+ - System packages: busybox, bash, zsh (with grml config), ripgrep, fd, dig, curl, git, unzip
110
110
  - mise package manager for additional runtime installations
111
111
  - Persistent storage for shell history, git config
112
112
  - Default editor: busybox vi
@@ -889,6 +889,7 @@ RUN apt update \
889
889
  fd-find ripgrep jq \
890
890
  iptables ipset dnsmasq dnsutils curl \
891
891
  build-essential git tmux \
892
+ unzip \
892
893
  && bash -c 'ln -s $(which fdfind) /usr/local/bin/fd' \
893
894
  && echo 'en_US.UTF-8 UTF-8' >> /etc/locale.gen \
894
895
  && echo 'ja_JP.UTF-8 UTF-8' >> /etc/locale.gen \
@@ -2,8 +2,6 @@
2
2
  * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "../agent"
3
3
  * @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs"
4
4
  * @import { Tool, SandboxModeProvider } from "../tool"
5
- * @import { VoiceInputConfig } from "../voice/input.mjs"
6
- * @import { VoiceSession } from "../voice/session.mjs"
7
5
  */
8
6
 
9
7
  import readline from "node:readline";
@@ -11,8 +9,6 @@ import { styleText } from "node:util";
11
9
  import { appendUsageRecord, buildUsageRecord } from "../usageStore.mjs";
12
10
  import { createSequentialExecutor } from "../utils/createSequentialExecutor.mjs";
13
11
  import { notify } from "../utils/notify.mjs";
14
- import { startVoiceSession } from "../voice/input.mjs";
15
- import { parseVoiceToggleKey } from "../voice/toggleKey.mjs";
16
12
  import { createCommandHandler } from "./commands.mjs";
17
13
  import { createCompleter, SLASH_COMMANDS } from "./completer.mjs";
18
14
  import {
@@ -21,7 +17,6 @@ import {
21
17
  printMessage,
22
18
  } from "./formatter.mjs";
23
19
  import { createInterruptTransform } from "./interruptTransform.mjs";
24
- import { createMuteTransform } from "./muteTransform.mjs";
25
20
  import { createPasteHandler } from "./pasteTransform.mjs";
26
21
  import { createStreamFormatter } from "./streamFormatter.mjs";
27
22
 
@@ -67,7 +62,6 @@ const HELP_MESSAGE = [
67
62
  * @property {boolean} sandbox
68
63
  * @property {() => Promise<void>} onStop
69
64
  * @property {ClaudeCodePlugin[]} [claudeCodePlugins]
70
- * @property {VoiceInputConfig} [voiceInput]
71
65
  * @property {Tool & SandboxModeProvider} [execCommandTool]
72
66
  */
73
67
 
@@ -112,7 +106,6 @@ export function startInteractiveSession({
112
106
  sandbox,
113
107
  onStop,
114
108
  claudeCodePlugins,
115
- voiceInput,
116
109
  execCommandTool,
117
110
  }) {
118
111
  /** @type {{ turn: boolean, multiLineBuffer: string[] | null, subagentName: string, toolSpinnerIndex: number, toolSpinnerLastTime: number }} */
@@ -127,19 +120,9 @@ export function startInteractiveSession({
127
120
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
128
121
  const SPINNER_INTERVAL_MS = 80;
129
122
 
130
- /**
131
- * Active voice input session, or null when not recording.
132
- * @type {{ session: VoiceSession, startCursor: number, transcriptLength: number } | null}
133
- */
134
- let voice = null;
135
-
136
123
  // Create the stream buffer instance for this session
137
124
  const streamBuffer = createStreamBuffer();
138
125
 
139
- // Parse the voice toggle key once at startup so misconfiguration fails
140
- // loudly instead of silently falling back.
141
- const voiceToggle = parseVoiceToggleKey(voiceInput?.toggleKey);
142
-
143
126
  const getCliPrompt = (subagentName = "", flashMessage = "") =>
144
127
  [
145
128
  "",
@@ -198,100 +181,7 @@ export function startInteractiveSession({
198
181
  cli.prompt();
199
182
  };
200
183
 
201
- const stopVoiceSession = async () => {
202
- if (!voice) return;
203
- const current = voice;
204
- voice = null;
205
- await current.session.stop();
206
- cli.setPrompt(currentCliPrompt);
207
- // @ts-expect-error - internal property
208
- cli._refreshLine?.();
209
- };
210
-
211
- const handleVoiceToggle = () => {
212
- // Ignore while the agent is working.
213
- if (!state.turn) return;
214
-
215
- if (voice) {
216
- stopVoiceSession();
217
- return;
218
- }
219
-
220
- if (!voiceInput) {
221
- cli.setPrompt(
222
- getCliPrompt(
223
- state.subagentName,
224
- styleText(
225
- "yellow",
226
- `Voice input not configured. Set \`voiceInput\` in your config to enable ${voiceToggle.label}.`,
227
- ),
228
- ),
229
- );
230
- cli.prompt(true);
231
- return;
232
- }
233
-
234
- const startCursor = cli.cursor;
235
- const session = startVoiceSession({
236
- config: voiceInput,
237
- callbacks: {
238
- onTranscript: (delta) => {
239
- if (!voice) return;
240
- const insertAt = voice.startCursor + voice.transcriptLength;
241
- // Insert delta at the recording's insertion point. User input is
242
- // swallowed while recording, so the buffer around `insertAt` is
243
- // stable.
244
- const before = cli.line.slice(0, insertAt);
245
- const after = cli.line.slice(insertAt);
246
- // `line` and `cursor` are declared readonly in the Node typings but
247
- // are writable at runtime — the existing code already patches
248
- // `_refreshLine` in the same way.
249
- const mutableCli = /** @type {{ line: string, cursor: number }} */ (
250
- /** @type {unknown} */ (cli)
251
- );
252
- mutableCli.line = before + delta + after;
253
- mutableCli.cursor = insertAt + delta.length;
254
- voice.transcriptLength += delta.length;
255
- // @ts-expect-error - internal property
256
- cli._refreshLine?.();
257
- },
258
- onError: (err) => {
259
- voice = null;
260
- cli.setPrompt(
261
- getCliPrompt(
262
- state.subagentName,
263
- styleText("red", `Voice input error: ${err.message}`),
264
- ),
265
- );
266
- cli.prompt(true);
267
- },
268
- onClose: () => {
269
- if (!voice) return;
270
- voice = null;
271
- cli.setPrompt(currentCliPrompt);
272
- // @ts-expect-error - internal property
273
- cli._refreshLine?.();
274
- },
275
- },
276
- });
277
- voice = { session, startCursor, transcriptLength: 0 };
278
- cli.setPrompt(
279
- getCliPrompt(
280
- state.subagentName,
281
- styleText(["red", "bold"], `● REC (${voiceToggle.label} to stop)`),
282
- ),
283
- );
284
- // @ts-expect-error - internal property
285
- cli._refreshLine?.();
286
- };
287
-
288
184
  const handleCtrlC = () => {
289
- // Stop voice recording first if active.
290
- if (voice) {
291
- stopVoiceSession();
292
- return;
293
- }
294
-
295
185
  // Agent turn: pause auto-approve; do not clear input.
296
186
  if (!state.turn) {
297
187
  agentCommands.pauseAutoApprove();
@@ -347,20 +237,14 @@ export function startInteractiveSession({
347
237
  };
348
238
 
349
239
  // Pre-readline pipeline:
350
- // stdin -> interrupt (Ctrl-C / Ctrl-D) -> mute (voice recording) -> paste (bracketed paste) -> readline
240
+ // stdin -> interrupt (Ctrl-C / Ctrl-D) -> paste (bracketed paste) -> readline
351
241
  const interrupt = createInterruptTransform({
352
242
  onCtrlC: handleCtrlC,
353
243
  onCtrlD: handleCtrlD,
354
- onVoiceToggle: handleVoiceToggle,
355
- voiceToggleByte: voiceToggle.byte,
356
244
  });
357
- // While a voice session is recording, swallow all stdin bytes other than
358
- // Ctrl-C / Ctrl-D / the voice toggle key so transcript insertion stays
359
- // consistent.
360
- const mute = createMuteTransform({ isMuted: () => voice !== null });
361
245
  const paste = createPasteHandler();
362
246
 
363
- process.stdin.pipe(interrupt).pipe(mute).pipe(paste.transform);
247
+ process.stdin.pipe(interrupt).pipe(paste.transform);
364
248
 
365
249
  // Enable bracketed paste mode
366
250
  if (process.stdout.isTTY) {
@@ -1,31 +1,21 @@
1
1
  import { Transform } from "node:stream";
2
2
 
3
3
  /**
4
- * Create a Transform that intercepts Ctrl-C (0x03), Ctrl-D (0x04), and an
5
- * optional "voice toggle" byte (default Ctrl-O, 0x0f). When one of those
6
- * bytes is seen anywhere in a chunk, the corresponding callback is invoked
7
- * and the entire chunk is dropped so that downstream consumers (e.g.
8
- * readline) never observe it. All other input flows through unchanged.
4
+ * Create a Transform that intercepts Ctrl-C (0x03) and Ctrl-D (0x04).
5
+ * When one of those bytes is seen anywhere in a chunk, the corresponding
6
+ * callback is invoked and the entire chunk is dropped so that downstream
7
+ * consumers (e.g. readline) never observe it. All other input flows
8
+ * through unchanged.
9
9
  *
10
10
  * Priority when multiple handled bytes appear in the same chunk:
11
- * Ctrl-C > Ctrl-D > voice toggle.
11
+ * Ctrl-C > Ctrl-D.
12
12
  *
13
13
  * @param {object} handlers
14
14
  * @param {() => void} handlers.onCtrlC - Called when Ctrl-C is detected
15
15
  * @param {() => void} handlers.onCtrlD - Called when Ctrl-D is detected
16
- * @param {() => void} [handlers.onVoiceToggle]
17
- * Called when the voice toggle byte is detected.
18
- * @param {number} [handlers.voiceToggleByte]
19
- * Byte value for the voice toggle key. Defaults to 0x0f (Ctrl-O).
20
16
  * @returns {Transform}
21
17
  */
22
- export function createInterruptTransform({
23
- onCtrlC,
24
- onCtrlD,
25
- onVoiceToggle,
26
- voiceToggleByte = 0x0f,
27
- }) {
28
- const voiceToggleChar = String.fromCharCode(voiceToggleByte);
18
+ export function createInterruptTransform({ onCtrlC, onCtrlD }) {
29
19
  return new Transform({
30
20
  transform(chunk, _encoding, callback) {
31
21
  const data = chunk.toString("utf8");
@@ -39,11 +29,6 @@ export function createInterruptTransform({
39
29
  callback();
40
30
  return;
41
31
  }
42
- if (onVoiceToggle && data.includes(voiceToggleChar)) {
43
- onVoiceToggle();
44
- callback();
45
- return;
46
- }
47
32
  this.push(chunk);
48
33
  callback();
49
34
  },
package/src/config.d.ts CHANGED
@@ -10,7 +10,6 @@ import {
10
10
  WebSearchToolGeminiOptions,
11
11
  WebSearchToolGeminiVertexAIOptions,
12
12
  } from "./tools/webSearch.mjs";
13
- import { VoiceInputConfig } from "./voice/input.mjs";
14
13
 
15
14
  /**
16
15
  * JSON-serializable webFetch configuration.
@@ -88,7 +87,6 @@ export type AppConfig = {
88
87
  };
89
88
  mcpServers?: Record<string, MCPServerConfig>;
90
89
  notifyCmd?: { command: string; args?: string[] };
91
- voiceInput?: VoiceInputConfig;
92
90
  claudeCodePlugins?: ClaudeCodePluginRepo[];
93
91
  };
94
92
 
package/src/config.mjs CHANGED
@@ -129,9 +129,6 @@ export async function loadAppConfig(options = {}) {
129
129
  ...(merged.claudeCodePlugins ?? []),
130
130
  ...(config.claudeCodePlugins ?? []),
131
131
  ],
132
- voiceInput: config.voiceInput
133
- ? { ...(merged.voiceInput ?? {}), ...config.voiceInput }
134
- : merged.voiceInput,
135
132
  };
136
133
  }
137
134
 
package/src/main.mjs CHANGED
@@ -447,7 +447,6 @@ export async function main(argv = process.argv) {
447
447
  execCommandTool,
448
448
  notifyCmd: appConfig.notifyCmd,
449
449
  claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
450
- voiceInput: appConfig.voiceInput,
451
450
  });
452
451
  }
453
452
  }
@@ -1,167 +0,0 @@
1
- ---
2
- description: Analyzes the project and generates sandbox configuration files (run.sh, setup.sh) tailored to the project's needs.
3
- ---
4
-
5
- You are a sandbox configurator. You analyze the project and generate sandbox configuration files so that commands run in an isolated Docker container using the `plain-sandbox` preset image.
6
-
7
- ## Overview
8
-
9
- You create the following files:
10
-
11
- - `.plain-agent/sandbox/run.sh` — Wrapper script for `plain-sandbox` with project-specific options
12
- - `.plain-agent/setup.sh` — Initial setup script for both sandbox and host
13
-
14
- ## Step 1: Analyze the Project
15
-
16
- Before generating anything, analyze the project to determine:
17
-
18
- ### 1a. Runtime & Tools
19
-
20
- Detect the project type and determine which runtimes to install via mise. Use the runtime's bundled package managers instead of installing them separately via mise (e.g. Node.js ships with npm; use `corepack enable` for yarn/pnpm).
21
-
22
- | File found | mise install commands | Version source |
23
- |---|---|---|
24
- | `package.json` | `mise use node@<version>` | `.nvmrc` / `.node-version` / `package.json` (`engines.node`) |
25
- | `requirements.txt` or `pyproject.toml` | `mise use python@<version>` | `.python-version` / `pyproject.toml` (`requires-python`) |
26
-
27
- Also check for common dev tools:
28
- - `*.tf` files or `.terraform-version` → `mise use terraform@<version>` (version source: `.terraform-version`)
29
-
30
- If a version cannot be determined from the files above, **ask the user which version to use** rather than falling back to a default.
31
-
32
- ### 1b. Volume Candidates
33
-
34
- Detect directories that should use Docker volumes. A Docker volume is preferred over a host bind mount for `node_modules` because:
35
-
36
- - `node_modules` contains many thousands of small files, and bind-mounting it into the container is slow on macOS/Windows (file sync overhead).
37
- - Native modules compiled for the host OS/arch can be incompatible with the Linux container, so keeping container-side `node_modules` isolated avoids conflicts.
38
-
39
- | Project type | Cache volumes | Dependency volumes |
40
- |---|---|---|
41
- | Node.js | `plain-sandbox--global--home-npm:/home/sandbox/.npm` | `node_modules` (per `package.json` dir if monorepo) |
42
- | Python | `plain-sandbox--global--home-pip:/home/sandbox/.cache/pip` | — |
43
-
44
- For monorepo detection: if multiple `package.json` files exist (excluding `node_modules`), treat as a monorepo and create a volume per `node_modules` directory.
45
-
46
- ### 1c. Setup Install Commands
47
-
48
- | Project type | Install command |
49
- |---|---|
50
- | Node.js (npm) | `npm ci` (or `npm install` if no lockfile) |
51
- | Node.js (yarn) | `corepack enable && yarn install --frozen-lockfile` |
52
- | Node.js (pnpm) | `corepack enable && pnpm install --frozen-lockfile` |
53
- | Python | `pip install -r requirements.txt` or `pip install .` |
54
-
55
- ## Step 2: Confirm with User
56
-
57
- Present the analysis results and ask the user to confirm. Show:
58
-
59
- 1. **Detected project type** (e.g., "Node.js with npm")
60
- 2. **mise install commands**
61
- 3. **Volume configuration** (e.g., "node_modules + npm cache")
62
- 4. **Setup install command** (e.g., "npm ci")
63
-
64
- Ask the following questions one at a time, waiting for the user's answer before proceeding to the next:
65
-
66
- 1. Do you want to mount `~/.gitconfig` into the sandbox? (This allows git commit inside the sandbox.)
67
- 2. Are there any commands that must run on the host instead of inside the container? (e.g. `gh`, `docker` — tools that require host credentials or sockets.) If so, which ones?
68
-
69
- ## Step 3: Generate run.sh
70
-
71
- Generate `.plain-agent/sandbox/run.sh`. Use the following Node.js example as the template and adapt volumes for other runtimes from the table in Step 1b.
72
-
73
- ```bash
74
- #!/usr/bin/env bash
75
-
76
- set -eu -o pipefail
77
-
78
- # Mount .plain-agent/ as read-only over the writable project root, then
79
- # re-overlay only memory/ and tmp/ as writable scratch space.
80
- working_dir=$(pwd)
81
- metadata_dir="$working_dir/.plain-agent"
82
- mkdir -p \
83
- "$metadata_dir/memory" \
84
- "$metadata_dir/tmp"
85
-
86
- options=(
87
- --allow-write
88
- --mount-readonly "$metadata_dir:$metadata_dir"
89
- --mount-writable "$metadata_dir/memory:$metadata_dir/memory"
90
- --mount-writable "$metadata_dir/tmp:$metadata_dir/tmp"
91
- --volume plain-sandbox--global--home-npm:/home/sandbox/.npm
92
- --volume node_modules
93
- )
94
-
95
- # Monorepo: create a volume for each node_modules directory.
96
- # Include only when multiple package.json files exist.
97
- # for path in $(fd package.json --max-depth 3 | sed -E 's,package.json$,node_modules,'); do
98
- # mkdir -p "$path"
99
- # options+=("--volume" "$path")
100
- # done
101
-
102
- # Mount main worktree if using git worktrees
103
- git_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
104
- if test -n "$git_root" && test -f "$git_root/.git"; then
105
- main_worktree_path=$(sed -E 's,^gitdir: (.+)/.git/.+,\1,' < "$git_root/.git")
106
- options+=("--mount-writable" "$main_worktree_path:$main_worktree_path")
107
- fi
108
-
109
- # Mount gitconfig (include only if the user confirmed)
110
- if test -f "$HOME/.gitconfig"; then
111
- options+=("--mount-readonly" "$HOME/.gitconfig:/home/sandbox/.gitconfig")
112
- fi
113
-
114
- plain-sandbox "${options[@]}" "$@"
115
- ```
116
-
117
- ## Step 4: Generate setup.sh
118
-
119
- Generate `.plain-agent/setup.sh`. Use the following Node.js example and replace `node@lts` / `npm ci` with the commands chosen in Step 1.
120
-
121
- ```bash
122
- #!/usr/bin/env bash
123
-
124
- set -eu -o pipefail
125
-
126
- this_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
127
-
128
- # Build Docker image and setup sandbox (install runtime and dependencies with network access)
129
- "$this_dir/sandbox/run.sh" --verbose --allow-net 0.0.0.0/0 mise use node@lts
130
- "$this_dir/sandbox/run.sh" --verbose --allow-net 0.0.0.0/0 npm ci
131
-
132
- # Setup host
133
- npm ci
134
- ```
135
-
136
- `--allow-net 0.0.0.0/0` is needed only during setup for downloading packages. It should NOT be in run.sh for normal usage.
137
-
138
- ## Step 5: Update config.json
139
-
140
- Show the user the following config and explain:
141
-
142
- - `--skip-build` assumes the image is already built (run `setup.sh` first to build)
143
- - `--keep-alive 30` reuses the container for 30 seconds between commands for performance
144
-
145
- If the user specified any unsandboxed commands in Step 2, also include and explain:
146
-
147
- - They run **unsandboxed** because they need host access.
148
-
149
- ```json
150
- {
151
- "sandbox": {
152
- "command": ".plain-agent/sandbox/run.sh",
153
- "args": ["--skip-build", "--keep-alive", "30"],
154
- "separator": "--",
155
- "rules": [
156
- {
157
- "pattern": { "command": { "$regex": "^(gh|docker)$" } },
158
- "mode": "unsandboxed"
159
- }
160
- ]
161
- }
162
- }
163
- ```
164
-
165
- Adjust the regex to match the commands the user specified. If none, omit `sandbox.rules`.
166
-
167
- After the user confirms, write the config into `.plain-agent/config.json` (merge if the file already exists).
@@ -1,60 +0,0 @@
1
- ---
2
- description: Update plain-agent configuration based on user needs.
3
- ---
4
-
5
- Fetch the latest README and help the user configure plain-agent for this project. Before each step, briefly explain to the user what you are about to do and why.
6
-
7
- ## Security Rule (Non-Negotiable)
8
-
9
- **Never write credentials** (API keys, tokens, passwords, secrets) into any config file.
10
-
11
- When a setting requires a credential:
12
- 1. Tell the user it must go into `.plain-agent/config.local.json`.
13
- 2. Show the exact JSON snippet they need to add.
14
- 3. Do not modify that file yourself.
15
-
16
- If the user wants a setting applied globally (`~/.config/plain-agent/`), show them the exact snippet and tell them to add it manually. Do not access the home directory.
17
-
18
- ## Step 1: Fetch the Latest README
19
-
20
- Fetch the latest README from GitHub as the authoritative reference for all configuration options:
21
-
22
- ```sh
23
- gh api --method GET -H "Accept: application/vnd.github.v3.raw" "repos/iinm/plain-agent/contents/README.md?ref=main"
24
- ```
25
-
26
- ## Step 2: Read the Current Config
27
-
28
- ```sh
29
- cat .plain-agent/config.json
30
- ```
31
-
32
- ## Step 3: Ask the User What They Want
33
-
34
- Ask what the user wants to configure. Common topics:
35
-
36
- - **Model** — which LLM to use (`model` field)
37
- - **Auto-approval rules** — which tool calls to allow automatically (`autoApproval`)
38
- - **Sandbox** — isolated execution environment (`sandbox`)
39
- - **MCP servers** — external tool integrations (`mcpServers`)
40
- - **Claude Code plugins** — reuse Claude Code plugin prompts/agents (`claudeCodePlugins`)
41
- - **Voice input** — voice transcription settings (`voiceInput`)
42
- - **Notifications** — custom notify command (`notifyCmd`)
43
-
44
- If the request is vague, ask a focused clarifying question before proceeding.
45
-
46
- **If the user wants to configure Sandbox**: immediately switch to the `sandbox-configurator` subagent and do not proceed further yourself.
47
-
48
- ## Step 4: Apply Changes
49
-
50
- Update `.plain-agent/config.json`. Rules:
51
-
52
- - Merge carefully — preserve all existing keys.
53
- - Only write to `.plain-agent/config.json`. Never access files outside the project directory.
54
- - For credential-requiring fields, use a placeholder like `"<YOUR_API_KEY>"` and instruct the user to add the real value to `.plain-agent/config.local.json` themselves.
55
-
56
- ## Step 5: Summarize
57
-
58
- 1. Show a diff or summary of what changed.
59
- 2. If any credentials were skipped, show the snippet the user needs to add to `config.local.json`.
60
- 3. Tell the user to restart `plain` for changes to take effect.