@iinm/plain-agent 1.8.6 → 1.8.7

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
@@ -29,9 +29,8 @@ A lightweight CLI-based coding agent.
29
29
 
30
30
  - Node.js 22 or later
31
31
  - LLM provider credentials
32
+ - [ripgrep](https://github.com/burntsushi/ripgrep), [fd](https://github.com/sharkdp/fd)
32
33
  - Bash / Docker for sandboxed execution
33
- - [ripgrep](https://github.com/burntsushi/ripgrep)
34
- - [fd](https://github.com/sharkdp/fd)
35
34
 
36
35
  ## Quick Start
37
36
 
@@ -50,8 +49,8 @@ Create the configuration.
50
49
  ```js
51
50
  // ~/.config/plain-agent/config.local.json
52
51
  {
52
+ // Set default model
53
53
  "model": "claude-sonnet-4-6+thinking-high",
54
- // "model": "gpt-5.5+thinking-high",
55
54
 
56
55
  // Configure the providers you want to use
57
56
  "platforms": [
@@ -59,7 +58,7 @@ Create the configuration.
59
58
  "name": "anthropic",
60
59
  "variant": "default",
61
60
  "apiKey": "<ANTHROPIC_API_KEY>"
62
- // Or
61
+ // Or read from environment variable
63
62
  // "apiKey": { "$env": "ANTHROPIC_API_KEY" }
64
63
  },
65
64
  {
@@ -71,17 +70,16 @@ Create the configuration.
71
70
  "name": "openai",
72
71
  "variant": "default",
73
72
  "apiKey": "<OPENAI_API_KEY>"
74
- },
73
+ }
75
74
  ],
76
75
 
77
- // Optional
76
+ // (Optional) Enable web search tools
78
77
  "tools": {
79
78
  // askWeb: Searches the web to answer questions requiring up-to-date information or external sources.
80
79
  "askWeb": {
81
80
  "provider": "gemini",
82
81
  "apiKey": "<GEMINI_API_KEY>",
83
82
  "model": "gemini-3-flash-preview"
84
-
85
83
  // Or use Vertex AI (Requires gcloud CLI to get authentication token)
86
84
  // "provider": "gemini-vertex-ai",
87
85
  // "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
@@ -89,18 +87,14 @@ Create the configuration.
89
87
  },
90
88
 
91
89
  // askURL: Answers questions based on provided URL content.
92
- // Directly injecting URL content into context is not supported to prevent prompt injection.
93
90
  "askURL": {
94
91
  "provider": "gemini",
95
- "apiKey": "<GEMINI_API_KEY>"
92
+ "apiKey": "<GEMINI_API_KEY>",
96
93
  "model": "gemini-3-flash-preview"
97
-
98
94
  // Or use Vertex AI (Requires gcloud CLI to get authentication token)
99
95
  }
100
- },
101
-
96
+ }
102
97
  }
103
-
104
98
  ```
105
99
 
106
100
  <details>
@@ -118,6 +112,7 @@ Create the configuration.
118
112
  "azureConfigDir": "/home/xxx/.azure-for-agent"
119
113
  },
120
114
  {
115
+ // Requires AWS CLI to get credentials
121
116
  "name": "bedrock",
122
117
  "variant": "default",
123
118
  "baseURL": "https://bedrock-runtime.<region>.amazonaws.com",
@@ -128,8 +123,8 @@ Create the configuration.
128
123
  "name": "vertex-ai",
129
124
  "variant": "default",
130
125
  "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project>/locations/<location>",
131
- // Optional
132
- "account": "<service_account_email>"
126
+ // (Optional) Impersonate this service account to obtain an auth token
127
+ "account": "<SERVICE_ACCOUNT_EMAIL>"
133
128
  }
134
129
  ]
135
130
  }
@@ -302,7 +297,7 @@ plain cost
302
297
  plain cost --from 2026-04-01 --to 2026-04-30
303
298
  ```
304
299
 
305
- (Optional) Configure plain-agent for your project.
300
+ Configure plain-agent for your project.
306
301
 
307
302
  ```
308
303
  /configure Auto-approve file writes and patches
@@ -417,14 +412,13 @@ Files are loaded in the following order. Settings in later files override earlie
417
412
 
418
413
  // MCP Tool naming convention: mcp__<serverName>__<toolName>
419
414
  {
420
- "toolName": { "$regex": "slack_(read|search)_.+" },
415
+ "toolName": { "$regex": "mcp__slack__slack_(read|search)_.+" },
421
416
  "action": "allow"
422
417
  }
423
418
  ]
424
419
  },
425
420
 
426
- // (Optional) Sandbox environment for the exec_command and tmux_command tools
427
- // https://github.com/iinm/plain-agent/tree/main/sandbox
421
+ // Sandbox environment for the exec_command and tmux_command tools
428
422
  "sandbox": {
429
423
  "command": "plain-sandbox",
430
424
  "args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
@@ -458,7 +452,7 @@ Files are loaded in the following order. Settings in later files override earlie
458
452
  "command": "npx",
459
453
  "args": ["-y", "chrome-devtools-mcp@latest", "--isolated"]
460
454
  },
461
- // ⚠️ Add this to config.local.json to avoid committing secrets to Git
455
+ // ⚠️ Add to config.local.json to avoid committing secrets to Git
462
456
  "slack": {
463
457
  "command": "npx",
464
458
  "args": ["-y", "mcp-remote", "https://mcp.slack.com/mcp", "--header", "Authorization:Bearer <SLACK_TOKEN>"],
@@ -475,21 +469,22 @@ Files are loaded in the following order. Settings in later files override earlie
475
469
  "command": "npx",
476
470
  "args": ["-y", "mcp-remote", "https://knowledge-mcp.global.api.aws"]
477
471
  },
478
- // ⚠️ Add this to config.local.json to avoid committing secrets to Git
472
+ // ⚠️ Add to config.local.json to avoid committing secrets to Git
479
473
  "google_developer-knowledge": {
480
474
  "command": "npx",
481
475
  "args": ["-y", "mcp-remote", "https://developerknowledge.googleapis.com/mcp", "--header", "X-Goog-Api-Key:<GOOGLE_API_KEY>"]
482
476
  }
483
477
  },
484
478
 
485
- // Override default notification command (falls back to terminal bell)
486
- // "notifyCmd": { "command": "plain-notify-desktop", "args": [] }
479
+ // Override default notification command
480
+ "notifyCmd": { "command": "plain-notify-desktop", "args": [] }
487
481
 
488
- // (Optional) Voice input. See "Voice Input" below.
489
- // "voiceInput": {
490
- // "provider": "openai",
491
- // "apiKey": "<OPENAI_API_KEY>"
492
- // }
482
+ // Voice input. See "Voice Input" below.
483
+ // ⚠️ Add to config.local.json to avoid committing secrets to Git
484
+ "voiceInput": {
485
+ "provider": "openai",
486
+ "apiKey": "<OPENAI_API_KEY>"
487
+ }
493
488
  }
494
489
  ```
495
490
  </details>
@@ -518,6 +513,7 @@ The agent searches for prompts in the following directories:
518
513
 
519
514
  - `~/.config/plain-agent/prompts/`
520
515
  - `.plain-agent/prompts/`
516
+ - `.plain-agent/prompts/skills/`
521
517
  - `.claude/commands/`
522
518
  - `.claude/skills/`
523
519
 
@@ -533,19 +529,6 @@ description: Create a commit message based on staged changes
533
529
  Review the staged changes and create a concise commit message following the conventional commits specification.
534
530
  ```
535
531
 
536
- You can also import remote prompts with the `import` field:
537
-
538
- ```md
539
- ---
540
- import: https://raw.githubusercontent.com/anthropics/claude-code/5cff78741f54a0dcfaeb11d29b9ea9a83f3882ff/plugins/feature-dev/commands/feature-dev.md
541
- ---
542
-
543
- - Use memory file instead of TodoWrite
544
- - Parallel execution of subagents is not supported. Delegate to subagents sequentially.
545
- ```
546
-
547
- Remote prompts are fetched and cached locally. The local content will be appended to the imported content.
548
-
549
532
  ### Shortcuts
550
533
 
551
534
  Prompts located in a `shortcuts/` subdirectory (e.g., `.plain-agent/prompts/shortcuts/commit.md`) can be invoked directly as a top-level command (e.g., `/commit`).
@@ -572,18 +555,6 @@ description: Simplifies and refines code for clarity and maintainability
572
555
  You are a code simplifier. Your role is to refactor code while preserving its functionality.
573
556
  ```
574
557
 
575
- You can also import remote subagent definitions with the `import` field:
576
-
577
- ```md
578
- ---
579
- import: https://raw.githubusercontent.com/anthropics/claude-code/f7ab5c799caf2ec8c7cd1b99d2bc2f158459ef5e/plugins/pr-review-toolkit/agents/code-simplifier.md
580
- ---
581
-
582
- Use AGENTS.md instead of CLAUDE.md in this project.
583
- ```
584
-
585
- Remote subagents are fetched and cached locally. The local content will be appended to the imported content.
586
-
587
558
  ## Claude Code Plugin Support
588
559
 
589
560
  Example:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.8.6",
3
+ "version": "1.8.7",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,10 +1,8 @@
1
1
  /** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
2
2
 
3
- import crypto from "node:crypto";
4
3
  import fs from "node:fs/promises";
5
4
  import path from "node:path";
6
5
  import {
7
- AGENT_CACHE_DIR,
8
6
  AGENT_PROJECT_METADATA_DIR,
9
7
  AGENT_ROOT,
10
8
  AGENT_USER_CONFIG_DIR,
@@ -18,7 +16,6 @@ import { parseFrontmatter } from "../utils/parseFrontmatter.mjs";
18
16
  * @property {string} content
19
17
  * @property {string} filePath
20
18
  * @property {boolean} claudeOriginated
21
- * @property {string} [import]
22
19
  */
23
20
 
24
21
  /**
@@ -81,15 +78,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
81
78
 
82
79
  if (content === null) return null;
83
80
 
84
- let role = parseAgentRole(file, content, fullPath, idPrefix);
85
- if (role.import) {
86
- try {
87
- role = await mergeRemoteRole(role, file, fullPath);
88
- } catch (err) {
89
- console.warn(`Failed to import remote role ${role.id}:`, err);
90
- return null;
91
- }
92
- }
81
+ const role = parseAgentRole(file, content, fullPath, idPrefix);
93
82
 
94
83
  return role;
95
84
  }),
@@ -100,99 +89,6 @@ export async function loadAgentRoles(claudeCodePlugins) {
100
89
  return new Map(roles.map((role) => [role.id, role]));
101
90
  }
102
91
 
103
- /**
104
- * Merges a remote role into a local role.
105
- * @param {AgentRole} localRole
106
- * @param {string} relativePath
107
- * @param {string} fullPath
108
- * @returns {Promise<AgentRole>}
109
- */
110
- async function mergeRemoteRole(localRole, relativePath, fullPath) {
111
- const importUrl = localRole.import;
112
- if (!importUrl) {
113
- return localRole;
114
- }
115
-
116
- const fetchedContent = await fetchAndCacheRole(importUrl).catch((err) => {
117
- console.warn(`Failed to fetch agent role from ${importUrl}:`, err);
118
- return null;
119
- });
120
-
121
- if (!fetchedContent) {
122
- return localRole;
123
- }
124
-
125
- const remoteRole = parseAgentRole(relativePath, fetchedContent, fullPath);
126
-
127
- return {
128
- ...remoteRole,
129
- ...localRole, // Local overrides
130
- content: `${remoteRole.content}\n\n---\n\n${localRole.content}`.trim(),
131
- description: localRole.description || remoteRole.description || "",
132
- };
133
- }
134
-
135
- /**
136
- * Fetch an agent role from a URL and cache it.
137
- * @param {string} url
138
- * @returns {Promise<string>}
139
- */
140
- async function fetchAndCacheRole(url) {
141
- const hash = crypto.createHash("sha256").update(url).digest("hex");
142
- const cacheDir = path.join(AGENT_CACHE_DIR, "agents");
143
- const cachePath = path.join(cacheDir, hash);
144
-
145
- const cachedContent = await fs.readFile(cachePath, "utf-8").catch(() => null);
146
- if (cachedContent !== null) {
147
- return cachedContent;
148
- }
149
-
150
- const fetchedContent = await fetchContent(url);
151
-
152
- // Attempt to cache, but don't block or fail on errors
153
- fs.mkdir(cacheDir, { recursive: true })
154
- .then(() => fs.writeFile(cachePath, fetchedContent, "utf-8"))
155
- .catch((err) => {
156
- console.warn(`Failed to write cache for ${url}:`, err);
157
- });
158
-
159
- return fetchedContent;
160
- }
161
-
162
- /**
163
- * Fetch content from a URL.
164
- * @param {string} url
165
- * @returns {Promise<string>}
166
- */
167
- async function fetchContent(url) {
168
- const githubMatch = url.match(
169
- /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/,
170
- );
171
-
172
- if (githubMatch) {
173
- const [, owner, repo, ref, path] = githubMatch;
174
- const apiUrl = `repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
175
- try {
176
- const { execFileSync } = await import("node:child_process");
177
- return execFileSync(
178
- "gh",
179
- ["api", "-H", "Accept: application/vnd.github.v3.raw", apiUrl],
180
- { encoding: "utf-8" },
181
- );
182
- } catch (err) {
183
- throw new Error(`Failed to fetch from GitHub via gh CLI: ${err}`);
184
- }
185
- }
186
-
187
- const response = await fetch(url);
188
- if (!response.ok) {
189
- throw new Error(
190
- `Failed to fetch agent role from ${url}: ${response.status} ${response.statusText}`,
191
- );
192
- }
193
- return response.text();
194
- }
195
-
196
92
  /**
197
93
  * Recursively get all markdown files in a directory.
198
94
  * @param {string} dir
@@ -262,6 +158,5 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
262
158
  content,
263
159
  filePath: fullPath,
264
160
  claudeOriginated,
265
- import: frontmatter.import,
266
161
  };
267
162
  }
@@ -1,11 +1,8 @@
1
1
  /** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
2
2
 
3
- import { execFileSync } from "node:child_process";
4
- import crypto from "node:crypto";
5
3
  import fs from "node:fs/promises";
6
4
  import path from "node:path";
7
5
  import {
8
- AGENT_CACHE_DIR,
9
6
  AGENT_PROJECT_METADATA_DIR,
10
7
  AGENT_ROOT,
11
8
  AGENT_USER_CONFIG_DIR,
@@ -19,7 +16,6 @@ import { parseFrontmatter } from "../utils/parseFrontmatter.mjs";
19
16
  * @property {string} content
20
17
  * @property {string} filePath
21
18
  * @property {boolean} claudeOriginated
22
- * @property {string} [import]
23
19
  * @property {boolean} [userInvocable]
24
20
  * @property {boolean} [isShortcut]
25
21
  * @property {boolean} [isSkill]
@@ -105,15 +101,7 @@ export async function loadPrompts(claudeCodePlugins) {
105
101
 
106
102
  if (content === null) return null;
107
103
 
108
- let prompt = parsePrompt(file, content, fullPath, idPrefix);
109
- if (prompt.import) {
110
- try {
111
- prompt = await mergeRemotePrompt(prompt, file, fullPath);
112
- } catch (err) {
113
- console.warn(`Failed to import remote prompt ${prompt.id}:`, err);
114
- return null;
115
- }
116
- }
104
+ const prompt = parsePrompt(file, content, fullPath, idPrefix);
117
105
 
118
106
  if (prompt.userInvocable === false) {
119
107
  return null;
@@ -127,98 +115,6 @@ export async function loadPrompts(claudeCodePlugins) {
127
115
  return new Map(prompts.map((prompt) => [prompt.id, prompt]));
128
116
  }
129
117
 
130
- /**
131
- * Merges a remote prompt into a local prompt if an import URL is provided.
132
- * @param {Prompt} localPrompt
133
- * @param {string} relativePath
134
- * @param {string} fullPath
135
- * @returns {Promise<Prompt>}
136
- */
137
- async function mergeRemotePrompt(localPrompt, relativePath, fullPath) {
138
- const importUrl = localPrompt.import;
139
- if (!importUrl) {
140
- return localPrompt;
141
- }
142
-
143
- const fetchedContent = await fetchAndCachePrompt(importUrl).catch((err) => {
144
- console.warn(`Failed to fetch prompt from ${importUrl}:`, err);
145
- return null;
146
- });
147
-
148
- if (!fetchedContent) {
149
- return localPrompt;
150
- }
151
-
152
- const remotePrompt = parsePrompt(relativePath, fetchedContent, fullPath);
153
-
154
- return {
155
- ...remotePrompt,
156
- ...localPrompt, // Local overrides
157
- content: `${remotePrompt.content}\n\n---\n\n${localPrompt.content}`.trim(),
158
- description: localPrompt.description || remotePrompt.description || "",
159
- };
160
- }
161
-
162
- /**
163
- * Fetch a prompt from a URL and cache it.
164
- * @param {string} url
165
- * @returns {Promise<string>}
166
- */
167
- async function fetchAndCachePrompt(url) {
168
- const hash = crypto.createHash("sha256").update(url).digest("hex");
169
- const cacheDir = path.join(AGENT_CACHE_DIR, "prompts");
170
- const cachePath = path.join(cacheDir, hash);
171
-
172
- const cachedContent = await fs.readFile(cachePath, "utf-8").catch(() => null);
173
- if (cachedContent !== null) {
174
- return cachedContent;
175
- }
176
-
177
- const fetchedContent = await fetchContent(url);
178
-
179
- // Attempt to cache, but don't block or fail on errors
180
- fs.mkdir(cacheDir, { recursive: true })
181
- .then(() => fs.writeFile(cachePath, fetchedContent, "utf-8"))
182
- .catch((err) => {
183
- console.warn(`Failed to write cache for ${url}:`, err);
184
- });
185
-
186
- return fetchedContent;
187
- }
188
-
189
- /**
190
- * Fetch content from a URL.
191
- * @param {string} url
192
- * @returns {Promise<string>}
193
- */
194
- async function fetchContent(url) {
195
- const githubMatch = url.match(
196
- /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/,
197
- );
198
-
199
- if (githubMatch) {
200
- const [, owner, repo, ref, path] = githubMatch;
201
- const apiUrl = `repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
202
- try {
203
- return execFileSync(
204
- "gh",
205
- ["api", "-H", "Accept: application/vnd.github.v3.raw", apiUrl],
206
- { encoding: "utf-8" },
207
- );
208
- } catch (err) {
209
- throw new Error(`Failed to fetch from GitHub via gh CLI: ${err}`);
210
- }
211
- }
212
-
213
- const response = await fetch(url);
214
- if (!response.ok) {
215
- throw new Error(
216
- `Failed to fetch prompt from ${url}: ${response.status} ${response.statusText}`,
217
- );
218
- }
219
- return response.text();
220
- }
221
-
222
118
  /**
223
119
  * Recursively get all markdown files in a directory.
224
120
  * @param {string} dir
@@ -295,7 +191,6 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
295
191
  content,
296
192
  filePath: fullPath,
297
193
  claudeOriginated,
298
- import: frontmatter.import,
299
194
  userInvocable: frontmatter["user-invocable"] === "true" ? true : undefined,
300
195
  isShortcut,
301
196
  isSkill: relativePath.endsWith("SKILL.md"),