@iinm/plain-agent 1.10.21 → 1.10.22

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.22",
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,
@@ -75,7 +73,6 @@ Always read the target lines with \`read_file\` first to verify line numbers and
75
73
 
76
74
  - Use relative paths for files inside the working directory, absolute paths for files outside.
77
75
  - Use ${projectMetadataDir}/tmp/ for temporary files.
78
- - Use bash -c only when pipes (|) or redirection (>, <) are required.
79
76
 
80
77
  Examples:
81
78
  - List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
@@ -84,18 +81,6 @@ Examples:
84
81
  Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
85
82
  Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
86
83
 
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
-
99
84
  # Project Rules and Skills
100
85
 
101
86
  Discover and apply project-specific rules and reusable skills.
@@ -119,7 +104,6 @@ ${skillDescriptions}
119
104
  - Current working directory: ${workingDir}
120
105
  - Today's date: ${today}
121
106
  - Session id: ${sessionId}
122
- - Tmux session id: ${tmuxSessionId}
123
107
  - Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
124
108
 
125
109
  Available subagents:
@@ -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 */