@iinm/plain-agent 1.10.21 → 1.10.23

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
@@ -360,7 +360,7 @@ Files are loaded in the following order. Settings in later files override earlie
360
360
  "action": "allow"
361
361
  },
362
362
  {
363
- "toolName": { "$regex": "^(exec_command|tmux_command)$" },
363
+ "toolName": "exec_command",
364
364
  "action": "allow"
365
365
  },
366
366
  {
@@ -426,6 +426,14 @@ Files are loaded in the following order. Settings in later files override earlie
426
426
  ]
427
427
  },
428
428
 
429
+ "tools": {
430
+ // Enable web tools. See Quick Start section.
431
+ "webSearch": {},
432
+ "webFetch": {},
433
+ // Enable the tmux tool
434
+ "tmux": { "enabled": true }
435
+ },
436
+
429
437
  // Sandbox environment for the exec_command and tmux_command tools
430
438
  "sandbox": {
431
439
  // Commands are wrapped and executed with this command
@@ -489,7 +497,7 @@ The agent can use the following tools:
489
497
  - **write_file**: Write a file.
490
498
  - **patch_file**: Patch a file.
491
499
  - **exec_command**: Run a command without shell interpretation.
492
- - **tmux_command**: Run a tmux command.
500
+ - **tmux_command**: Run a tmux command. It is disabled by default.
493
501
  - **web_search**: Search the web with one or more keyword sets and answer a question based on the combined results (requires Google API key, Vertex AI configuration, or the `command` provider with a local search command).
494
502
  - **web_fetch**: Fetch the contents of a single URL and answer a question based on it (requires Google API key, Vertex AI configuration, or the `command` provider with a local fetch command such as `w3m`, `curl`, or `lynx`).
495
503
  - **switch_to_subagent**: Switch to a subagent role within the same conversation, focusing on the specified goal.
@@ -9,6 +9,15 @@
9
9
  "action": "deny",
10
10
  "reason": "Use rg or fd instead"
11
11
  },
12
+ {
13
+ "toolName": "exec_command",
14
+ "input": {
15
+ "command": "bash",
16
+ "args": ["-c", { "$not": { "$regex": "[|><&;$`]" } }]
17
+ },
18
+ "action": "deny",
19
+ "reason": "Use bash -c only when shell features (|, >, <, &, ;, $, `) are required"
20
+ },
12
21
  {
13
22
  "toolName": "exec_command",
14
23
  "input": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.10.21",
3
+ "version": "1.10.23",
4
4
  "description": "A lightweight coding agent for the terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/config.d.ts CHANGED
@@ -83,6 +83,7 @@ export type AppConfig = {
83
83
  tools?: {
84
84
  webSearch?: WebSearchToolConfig;
85
85
  webFetch?: WebFetchToolConfig;
86
+ tmux?: { enabled: boolean };
86
87
  };
87
88
  mcpServers?: Record<string, MCPServerConfig>;
88
89
  notifyCmd?: { command: string; args?: string[] };
package/src/config.mjs CHANGED
@@ -95,6 +95,12 @@ export async function loadAppConfig(options = {}) {
95
95
  ...config.tools.webFetch,
96
96
  }
97
97
  : merged.tools?.webFetch,
98
+ tmux: config.tools?.tmux
99
+ ? {
100
+ ...(merged.tools?.tmux ?? {}),
101
+ ...config.tools.tmux,
102
+ }
103
+ : merged.tools?.tmux,
98
104
  },
99
105
  mcpServers: {
100
106
  ...(merged.mcpServers ?? {}),
package/src/main.mjs CHANGED
@@ -139,8 +139,6 @@ export async function main(argv = process.argv) {
139
139
  ? new Date(resumedState.startTime)
140
140
  : new Date();
141
141
  const sessionId = resumedState ? resumedState.sessionId : generateSessionId();
142
- const tmuxSessionId = `agent-${sessionId}`;
143
-
144
142
  const isBatchMode = cliArgs.subcommand.type === "batch";
145
143
  /** @type {string[]} */
146
144
  const configFiles =
@@ -294,7 +292,6 @@ export async function main(argv = process.argv) {
294
292
  workingDir: process.cwd(),
295
293
  today: new Date().toISOString().split("T")[0],
296
294
  sessionId,
297
- tmuxSessionId,
298
295
  projectMetadataDir: AGENT_PROJECT_METADATA_DIR,
299
296
  agentRoles,
300
297
  skills: Array.from(prompts.values()).filter((p) => p.isSkill),
@@ -305,12 +302,15 @@ export async function main(argv = process.argv) {
305
302
  readFileTool,
306
303
  writeFileTool,
307
304
  createPatchFileTool(),
308
- createTmuxCommandTool({ sandbox: appConfig.sandbox }),
309
305
  createCompactContextTool(),
310
306
  createSwitchToSubagentTool(),
311
307
  createSwitchToMainAgentTool(),
312
308
  ];
313
309
 
310
+ if (appConfig.tools?.tmux?.enabled) {
311
+ builtinTools.push(createTmuxCommandTool({ sandbox: appConfig.sandbox }));
312
+ }
313
+
314
314
  if (appConfig.tools?.webSearch) {
315
315
  const webSearchConfig = appConfig.tools.webSearch;
316
316
  if (webSearchConfig.provider === "command") {
package/src/prompt.mjs CHANGED
@@ -7,7 +7,6 @@ import { toOneLine } from "./utils/toOneLine.mjs";
7
7
  * @property {string} workingDir - The current working directory.
8
8
  * @property {string} today - Today's date in YYYY-MM-DD format.
9
9
  * @property {string} sessionId
10
- * @property {string} tmuxSessionId
11
10
  * @property {string} projectMetadataDir - The directory where memory files are stored.
12
11
  * @property {Map<string, import('./context/loadAgentRoles.mjs').AgentRole>} agentRoles - Available agent roles.
13
12
  * @property {{filePath: string, description: string}[]} skills
@@ -22,7 +21,6 @@ export function createPrompt({
22
21
  modelName,
23
22
  sessionId,
24
23
  today,
25
- tmuxSessionId,
26
24
  workingDir,
27
25
  projectMetadataDir,
28
26
  agentRoles,
@@ -51,50 +49,24 @@ Respond in the user's language.
51
49
 
52
50
  # Memory Files
53
51
 
54
- Memory files preserve task state so work can be resumed after a context reset.
52
+ Memory files preserve state to resume work after context resets.
55
53
 
56
- - Create/Update memory files when creating/updating a plan, completing milestones, encountering issues, or making decisions. Skip memory files for tasks that can be completed in a few steps.
57
- - Ensure self-containment: Write as if the reader has no prior knowledge of the conversation.
54
+ - Create/Update memory files when creating/updating a plan, completing milestones, encountering issues, or making decisions.
55
+ - Skip memory files for tasks that can be completed in a few steps.
58
56
  - Write the memory content in the user's language.
59
57
 
60
58
  Memory files should include:
61
59
  - Task overview: What the task is, why it's being done, requirements and constraints
62
- - References: AGENTS.md, skills, relevant documentation, source files, and commands
63
- - Progress tracking: Completed milestones with evidence, current status, and next steps
64
- - Decision records: Key decisions, alternatives considered, and rationale
60
+ - References: AGENTS.md, documentation, source files, commands
61
+ - Progress tracking: Completed milestones with results, current status, and next steps
62
+ - Decision records: Key decisions, alternatives considered, and reason
65
63
 
66
64
  # Tools
67
65
 
68
- Call multiple tools at once when they don't depend on each other's results.
69
-
70
- ## patch_file
71
-
72
- Always read the target lines with \`read_file\` first to verify line numbers and their 2-char hashes before calling \`patch_file\`.
73
-
74
- ## exec_command
75
-
66
+ - Run independent tools in parallel.
67
+ - Verify line numbers and hashes with read_file before calling patch_file.
76
68
  - Use relative paths for files inside the working directory, absolute paths for files outside.
77
69
  - Use ${projectMetadataDir}/tmp/ for temporary files.
78
- - Use bash -c only when pipes (|) or redirection (>, <) are required.
79
-
80
- Examples:
81
- - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
82
- - Search for strings: rg ["--heading", "--line-number", "pattern", "./"]
83
- - Manage GitHub issues and PRs:
84
- Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
85
- Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
86
-
87
- ## tmux_command
88
-
89
- - Use only when the user explicitly requests it.
90
- - Create a new session with the given tmux session id.
91
-
92
- Examples:
93
- - Start session: new-session ["-d", "-s", "<tmux-session-id>"]
94
- - Detect window number to send keys: list-windows ["-t", "<tmux-session-id>"]
95
- - Get output of window before sending keys: capture-pane ["-p", "-t", "<tmux-session-id>:<window>"]
96
- - Send key to session: send-keys ["-t", "<tmux-session-id>:<window>", "echo hello", "Enter"]
97
- - Delete line: send-keys ["-t", "<tmux-session-id>:<window>", "C-a", "C-k"]
98
70
 
99
71
  # Project Rules and Skills
100
72
 
@@ -114,13 +86,12 @@ ${skillDescriptions}
114
86
 
115
87
  # Environment
116
88
 
89
+ - Session id: ${sessionId}
90
+ - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
117
91
  - User name: ${username}
118
92
  - Your model name: ${modelName}
119
93
  - Current working directory: ${workingDir}
120
94
  - Today's date: ${today}
121
- - Session id: ${sessionId}
122
- - Tmux session id: ${tmuxSessionId}
123
- - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
124
95
 
125
96
  Available subagents:
126
97
  ${agentRoleDescriptions}
@@ -20,7 +20,15 @@ export function createExecCommandTool(config) {
20
20
  return {
21
21
  def: {
22
22
  name: "exec_command",
23
- description: "Run a command without shell interpretation.",
23
+ description: `Run a command without shell interpretation.
24
+
25
+ Examples:
26
+ - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
27
+ - Search for strings: rg ["--heading", "--line-number", "pattern", "./"]
28
+ - Manage GitHub issues and PRs:
29
+ Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
30
+ Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
31
+ `.trim(),
24
32
  inputSchema: {
25
33
  type: "object",
26
34
  properties: {
@@ -29,9 +37,7 @@ export function createExecCommandTool(config) {
29
37
  type: "string",
30
38
  },
31
39
  args: {
32
- // Gemini 3 flashが command: rg, args: [rg, ...] のようにargsにコマンドを含めることがある
33
- description:
34
- "Array of arguments to pass to the command. Do not include the command name itself in this array.",
40
+ description: "Array of arguments to pass to the command.",
35
41
  type: "array",
36
42
  items: {
37
43
  type: "string",
@@ -18,7 +18,17 @@ export function createTmuxCommandTool(config) {
18
18
  return {
19
19
  def: {
20
20
  name: "tmux_command",
21
- description: "Run a tmux command",
21
+ description: [
22
+ "Run a tmux command.",
23
+ "The tmux session id is plain-agent-<session-id>.",
24
+ "",
25
+ "Examples:",
26
+ '- Start session: new-session ["-d", "-s", "<tmux-session-id>"]',
27
+ '- Detect window number to send keys: list-windows ["-t", "<tmux-session-id>"]',
28
+ '- Get output of window before sending keys: capture-pane ["-p", "-t", "<tmux-session-id>:<window>"]',
29
+ '- Send key to session: send-keys ["-t", "<tmux-session-id>:<window>", "echo hello", "Enter"]',
30
+ '- Delete line: send-keys ["-t", "<tmux-session-id>:<window>", "C-a", "C-k"]',
31
+ ].join("\n"),
22
32
  inputSchema: {
23
33
  type: "object",
24
34
  properties: {
@@ -40,6 +40,23 @@ export function evalJSONConfig(configItem) {
40
40
  );
41
41
  }
42
42
 
43
+ if (Object.keys(configItem).length === 1 && "$not" in configItem) {
44
+ const pattern = evalJSONConfig(configItem.$not);
45
+ /** @param {unknown} value */
46
+ return (value) => {
47
+ if (typeof pattern === "string") {
48
+ return value !== pattern;
49
+ }
50
+ if (pattern instanceof RegExp) {
51
+ return typeof value !== "string" || !pattern.test(value);
52
+ }
53
+ if (typeof pattern === "function") {
54
+ return !pattern(value);
55
+ }
56
+ return true;
57
+ };
58
+ }
59
+
43
60
  if (Object.keys(configItem).length === 1 && "$has" in configItem) {
44
61
  const pattern = evalJSONConfig(configItem.$has);
45
62
  /** @param {unknown} value */