@iinm/plain-agent 1.2.1 → 1.3.1

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
@@ -33,7 +33,7 @@ npm install -g @iinm/plain-agent
33
33
  List available models.
34
34
 
35
35
  ```sh
36
- plain --list-models
36
+ plain list-models
37
37
  ```
38
38
 
39
39
  Create the configuration.
@@ -122,7 +122,7 @@ Create the configuration.
122
122
  ```
123
123
 
124
124
  <details>
125
- <summary>Other provider examples</summary>
125
+ <summary><b>Other provider examples</b></summary>
126
126
 
127
127
  ```js
128
128
  {
@@ -149,6 +149,59 @@ Create the configuration.
149
149
  ```
150
150
  </details>
151
151
 
152
+ <details>
153
+ <summary><b>Bedrock example using Claude Japan inference profiles</b></summary>
154
+
155
+ ```js
156
+ {
157
+ "models": [
158
+ {
159
+ "name": "claude-haiku-4-5",
160
+ "variant": "thinking-16k-bedrock-jp",
161
+ "platform": {
162
+ "name": "bedrock",
163
+ "variant": "jp"
164
+ },
165
+ "model": {
166
+ "format": "anthropic",
167
+ "config": {
168
+ "model": "jp.anthropic.claude-haiku-4-5-20251001-v1:0",
169
+ "max_tokens": 32768,
170
+ "thinking": { "type": "enabled", "budget_tokens": 16384 }
171
+ }
172
+ }
173
+ },
174
+ {
175
+ "name": "claude-sonnet-4-6",
176
+ "variant": "thinking-16k-bedrock-jp",
177
+ "platform": {
178
+ "name": "bedrock",
179
+ "variant": "jp"
180
+ },
181
+ "model": {
182
+ "format": "anthropic",
183
+ "config": {
184
+ "model": "jp.anthropic.claude-sonnet-4-6",
185
+ "max_tokens": 32768,
186
+ "thinking": { "type": "enabled", "budget_tokens": 16384 }
187
+ }
188
+ }
189
+ }
190
+ ],
191
+ "platforms": [
192
+ {
193
+ "name": "bedrock",
194
+ "variant": "jp",
195
+ "baseURL": "https://bedrock-runtime.ap-northeast-1.amazonaws.com",
196
+ "awsProfile": "FIXME"
197
+ }
198
+ ]
199
+ }
200
+ ```
201
+ </details>
202
+
203
+
204
+
152
205
  Run the agent.
153
206
 
154
207
  ```sh
@@ -162,7 +215,7 @@ Run in batch mode (non-interactive).
162
215
  In batch mode, config files are not loaded automatically. Only the files specified with `--config` are loaded.
163
216
 
164
217
  ```sh
165
- plain --batch "Add tests for src/main.mjs" \
218
+ plain batch "Add tests for src/main.mjs" \
166
219
  --config ~/.config/plain-agent/config.local.json \
167
220
  --config .plain-agent/config.json
168
221
  ```
@@ -223,7 +276,7 @@ The agent loads configuration files in the following order. Settings in later fi
223
276
  ### Example
224
277
 
225
278
  <details>
226
- <summary>YOLO mode example (requires sandbox for safety)</summary>
279
+ <summary><b>YOLO mode example (requires sandbox for safety)</b></summary>
227
280
 
228
281
  ```js
229
282
  {
@@ -270,7 +323,7 @@ The agent loads configuration files in the following order. Settings in later fi
270
323
  </details>
271
324
 
272
325
  <details>
273
- <summary>Full example</summary>
326
+ <summary><b>Full example</b></summary>
274
327
 
275
328
  ```js
276
329
  {
@@ -467,20 +520,29 @@ The agent searches for subagent definitions in the following directories:
467
520
 
468
521
  Example:
469
522
 
470
- ```sh
471
- git clone --depth 1 https://github.com/anthropics/claude-code .plain-agent/claude-code-plugins/anthropics/claude-code
472
- git clone --depth 1 https://github.com/awslabs/agent-plugins .plain-agent/claude-code-plugins/awslabs/agent-plugins
473
- ```
474
-
475
523
  ```js
476
524
  // .plain-agent/config.json
477
525
  {
478
526
  "claudeCodePlugins": [
479
- { "name": "pr-review-toolkit", "path": "anthropics/claude-code/plugins/pr-review-toolkit" },
480
- { "name": "aws-serverless", "path": "awslabs/agent-plugins/plugins/aws-serverless" }
527
+ {
528
+ "source": "https://github.com/anthropics/claude-code",
529
+ "plugins": [
530
+ { "name": "feature-dev", "path": "plugins/feature-dev" },
531
+ { "name": "code-review", "path": "plugins/code-review" }
532
+ ]
533
+ },
534
+ {
535
+ "source": "https://github.com/anthropics/skills",
536
+ "plugins": [
537
+ { "name": "anthropics", "path": "", "only": "pdf|pptx" }
538
+ ]
539
+ }
481
540
  ]
482
541
  }
542
+ ```
483
543
 
544
+ ```sh
545
+ plain install-claude-code-plugins
484
546
  ```
485
547
 
486
548
  ## Development
@@ -502,7 +564,7 @@ npx npm-check-updates -t minor -c 3 -u
502
564
  ## Appendix: Creating Least-Privilege Users for Cloud Providers
503
565
 
504
566
  <details>
505
- <summary>Amazon Bedrock</summary>
567
+ <summary><b>Amazon Bedrock</b></summary>
506
568
 
507
569
  ```sh
508
570
  # IAM Identity Center
@@ -582,7 +644,7 @@ aws bedrock-runtime invoke-model \
582
644
  </details>
583
645
 
584
646
  <details>
585
- <summary>Azure - Microsoft Foundry</summary>
647
+ <summary><b>Azure - Microsoft Foundry</b></summary>
586
648
 
587
649
  ```sh
588
650
  resource_group=FIXME
@@ -615,7 +677,7 @@ az login --service-principal -u "$app_id" -p "$app_secret" --tenant "$tenant_id"
615
677
  </details>
616
678
 
617
679
  <details>
618
- <summary>Google Cloud Vertex AI</summary>
680
+ <summary><b>Google Cloud Vertex AI</b></summary>
619
681
 
620
682
  ```sh
621
683
  project_id=FIXME
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,164 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { loadAppConfig } from "./config.mjs";
5
+ import { CLAUDE_CODE_PLUGIN_DIR } from "./env.mjs";
6
+
7
+ /**
8
+ * @typedef {Object} ClaudeCodePluginRepo
9
+ * @property {string} source
10
+ * @property {Array<{name: string, path: string, only?: string}>} plugins
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} ClaudeCodePlugin
15
+ * @property {string} name
16
+ * @property {string} path
17
+ * @property {RegExp} [only]
18
+ */
19
+
20
+ /**
21
+ * Resolve plugin paths from hierarchical config structure.
22
+ * Converts {source, plugins} to flat {name, path, only} with full paths.
23
+ * @param {ClaudeCodePluginRepo[]} repos
24
+ * @returns {ClaudeCodePlugin[]}
25
+ */
26
+ export function resolvePluginPaths(repos) {
27
+ if (!repos) return [];
28
+
29
+ /** @type {ClaudeCodePlugin[]} */
30
+ const result = [];
31
+
32
+ for (const repo of repos) {
33
+ const ownerRepo = extractOwnerRepo(repo.source);
34
+ if (!ownerRepo) {
35
+ console.warn(`Invalid source URL: ${repo.source}`);
36
+ continue;
37
+ }
38
+
39
+ for (const plugin of repo.plugins) {
40
+ // Compile only pattern to RegExp
41
+ let only;
42
+ if (plugin.only) {
43
+ try {
44
+ only = new RegExp(plugin.only);
45
+ } catch (err) {
46
+ console.warn(
47
+ `Invalid regex pattern "${plugin.only}" for plugin "${plugin.name}":`,
48
+ err instanceof Error ? err.message : String(err),
49
+ );
50
+ }
51
+ }
52
+
53
+ result.push({
54
+ name: plugin.name,
55
+ path: path.join(CLAUDE_CODE_PLUGIN_DIR, ownerRepo, plugin.path),
56
+ only,
57
+ });
58
+ }
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Install Claude Code plugins by cloning repositories.
66
+ */
67
+ export async function installClaudeCodePlugins() {
68
+ const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
69
+ const repos = appConfig.claudeCodePlugins ?? [];
70
+
71
+ if (repos.length === 0) {
72
+ console.log("No plugins configured.");
73
+ return;
74
+ }
75
+
76
+ let installed = 0;
77
+ let skipped = 0;
78
+ let failed = 0;
79
+
80
+ // Track paths for summary
81
+ /** @type {string[]} */
82
+ const installedPaths = [];
83
+ /** @type {string[]} */
84
+ const skippedPaths = [];
85
+
86
+ // Ensure plugin directory exists
87
+ await fs.mkdir(CLAUDE_CODE_PLUGIN_DIR, { recursive: true });
88
+
89
+ for (const repo of repos) {
90
+ const ownerRepo = extractOwnerRepo(repo.source);
91
+ if (!ownerRepo) {
92
+ console.error(`❌ Invalid source URL: ${repo.source}`);
93
+ failed++;
94
+ continue;
95
+ }
96
+
97
+ const destPath = path.join(CLAUDE_CODE_PLUGIN_DIR, ownerRepo);
98
+
99
+ // Check if already exists
100
+ const exists = await fs
101
+ .access(destPath)
102
+ .then(() => true)
103
+ .catch(() => false);
104
+ if (exists) {
105
+ console.log(`⏭️ Skipping ${repo.source} → ${destPath}: already installed`);
106
+ skippedPaths.push(destPath);
107
+ skipped++;
108
+ continue;
109
+ }
110
+
111
+ // Clone the repository
112
+ console.log(`📥 Installing ${repo.source}...`);
113
+ try {
114
+ await new Promise((resolve, reject) => {
115
+ execFile(
116
+ "git",
117
+ ["clone", "--depth", "1", repo.source, destPath],
118
+ (err) => {
119
+ if (err) reject(err);
120
+ else resolve(undefined);
121
+ },
122
+ );
123
+ });
124
+ console.log(`✅ Installed to ${destPath}`);
125
+ installedPaths.push(destPath);
126
+ installed++;
127
+ } catch (error) {
128
+ console.error(
129
+ `❌ Failed to install: ${error instanceof Error ? error.message : String(error)}`,
130
+ );
131
+ failed++;
132
+ }
133
+ }
134
+
135
+ console.log(
136
+ `\n📊 Summary: ${installed} installed, ${skipped} skipped, ${failed} failed`,
137
+ );
138
+
139
+ if (installedPaths.length > 0) {
140
+ console.log("\nInstalled:");
141
+ for (const p of installedPaths) {
142
+ console.log(` • ${p}`);
143
+ }
144
+ }
145
+
146
+ if (skippedPaths.length > 0) {
147
+ console.log("\nSkipped:");
148
+ for (const p of skippedPaths) {
149
+ console.log(` • ${p}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Extract owner/repo from source URL.
156
+ * @param {string} source
157
+ * @returns {string|null}
158
+ */
159
+ function extractOwnerRepo(source) {
160
+ // Handle: https://github.com/owner/repo
161
+ // Handle: git@github.com:owner/repo.git
162
+ const match = source.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
163
+ return match ? match[1] : null;
164
+ }
package/src/cliArgs.mjs CHANGED
@@ -1,10 +1,30 @@
1
+ /**
2
+ * @typedef {HelpSubcommand | InteractiveSubcommand | BatchSubcommand | ListModelsSubcommand | InstallClaudeCodePluginsSubcommand} Subcommand
3
+ */
4
+
5
+ /**
6
+ * @typedef {{ type: 'help' }} HelpSubcommand
7
+ */
8
+
9
+ /**
10
+ * @typedef {{ type: 'interactive', config: string[], model: string | null }} InteractiveSubcommand
11
+ */
12
+
13
+ /**
14
+ * @typedef {{ type: 'batch', task: string, config: string[], model: string | null }} BatchSubcommand
15
+ */
16
+
17
+ /**
18
+ * @typedef {{ type: 'list-models' }} ListModelsSubcommand
19
+ */
20
+
21
+ /**
22
+ * @typedef {{ type: 'install-claude-code-plugins' }} InstallClaudeCodePluginsSubcommand
23
+ */
24
+
1
25
  /**
2
26
  * @typedef {Object} CliArgs
3
- * @property {string|null} model - Model name with variant
4
- * @property {boolean} showHelp - Whether to show help message
5
- * @property {boolean} listModels - Whether to list available models
6
- * @property {string|null} batch - Task instruction for batch mode
7
- * @property {string[]} config - Paths to additional config files for batch mode
27
+ * @property {Subcommand} subcommand - The subcommand to execute
8
28
  */
9
29
 
10
30
  /**
@@ -14,30 +34,81 @@
14
34
  */
15
35
  export function parseCliArgs(argv) {
16
36
  const args = argv.slice(2);
17
- /** @type {CliArgs} */
18
- const result = {
19
- model: null,
20
- showHelp: false,
21
- listModels: false,
22
- batch: null,
23
- config: [],
24
- };
37
+ const subcommandName = args[0];
38
+
39
+ if (["-h", "--help", "help"].includes(subcommandName)) {
40
+ return {
41
+ subcommand: { type: "help" },
42
+ };
43
+ }
25
44
 
26
- for (let i = 0; i < args.length; i++) {
27
- if ((args[i] === "-m" || args[i] === "--model") && args[i + 1]) {
28
- result.model = args[++i];
29
- } else if (args[i] === "-h" || args[i] === "--help") {
30
- result.showHelp = true;
31
- } else if (args[i] === "-l" || args[i] === "--list-models") {
32
- result.listModels = true;
33
- } else if (args[i] === "--batch" && args[i + 1]) {
34
- result.batch = args[++i];
35
- } else if (args[i] === "--config" && args[i + 1]) {
36
- result.config.push(args[++i]);
45
+ if (!subcommandName || subcommandName.startsWith("-")) {
46
+ // Interactive mode (default)
47
+ const config = [];
48
+ let model = null;
49
+
50
+ for (let i = 0; i < args.length; i++) {
51
+ if (args[i] === "-m" || args[i] === "--model") {
52
+ if (args[i + 1]) {
53
+ model = args[i + 1];
54
+ i++;
55
+ }
56
+ } else if (args[i] === "-c" || args[i] === "--config") {
57
+ if (args[i + 1]) {
58
+ config.push(args[i + 1]);
59
+ i++;
60
+ }
61
+ }
37
62
  }
63
+
64
+ return {
65
+ subcommand: { type: "interactive", config, model },
66
+ };
38
67
  }
39
68
 
40
- return result;
69
+ if (subcommandName === "batch") {
70
+ const batchArgs = args.slice(1);
71
+
72
+ let task = null;
73
+ let model = null;
74
+ const config = [];
75
+
76
+ for (let i = 0; i < batchArgs.length; i++) {
77
+ if (batchArgs[i] === "-m" || batchArgs[i] === "--model") {
78
+ if (batchArgs[i + 1]) {
79
+ model = batchArgs[i + 1];
80
+ i++;
81
+ }
82
+ } else if (batchArgs[i] === "-c" || batchArgs[i] === "--config") {
83
+ if (batchArgs[i + 1]) {
84
+ config.push(batchArgs[i + 1]);
85
+ i++;
86
+ }
87
+ } else if (!batchArgs[i].startsWith("-") && !task) {
88
+ task = batchArgs[i];
89
+ }
90
+ }
91
+
92
+ return {
93
+ subcommand: { type: "batch", task: task || "", config, model },
94
+ };
95
+ }
96
+
97
+ if (subcommandName === "list-models") {
98
+ return {
99
+ subcommand: { type: "list-models" },
100
+ };
101
+ }
102
+
103
+ if (subcommandName === "install-claude-code-plugins") {
104
+ return {
105
+ subcommand: { type: "install-claude-code-plugins" },
106
+ };
107
+ }
108
+
109
+ return {
110
+ subcommand: { type: "help" },
111
+ };
41
112
  }
42
113
 
43
114
  /**
@@ -46,22 +117,29 @@ export function parseCliArgs(argv) {
46
117
  */
47
118
  export function printHelp(exitCode = 0) {
48
119
  console.log(`
49
- Usage: agent [options]
50
- agent --batch "task instruction" [options]
120
+ Usage: plain [options]
121
+ plain batch [options] <task>
122
+ plain list-models
123
+ plain install-claude-code-plugins
51
124
 
52
125
  Options:
53
126
  -m, --model <model+variant> Model to use
54
- -l, --list-models List available models
55
- -h, --help Show this help message
56
- --batch <task> Run in batch mode with the given task instruction
57
- --config <file> Config file to load (required in batch mode)
58
- In batch mode, only explicitly specified config files are loaded
127
+ -h, --help Show this help message
128
+ -c, --config <file> Config file to load
129
+
130
+ Subcommands:
131
+ batch <task> Run in batch mode with the given task instruction
132
+ list-models List available models
133
+ install-claude-code-plugins Install Claude Code plugins
59
134
 
60
135
  Examples:
61
- agent -m gpt-5.4+thinking-medium
62
- plain --batch "Add tests for src/main.mjs" \\
63
- --config ~/.config/plain-agent/config.local.json \\
64
- --config .plain-agent/config.json
136
+ plain -m gpt-5.4+thinking-medium
137
+ plain batch \\
138
+ -c ~/.config/plain-agent/config.local.json \\
139
+ -c .plain-agent/config.json \\
140
+ "Add tests for src/main.mjs"
141
+ plain list-models
142
+ plain install-claude-code-plugins
65
143
  `);
66
144
  process.exit(exitCode);
67
145
  }
@@ -192,7 +192,7 @@ export function formatProviderTokenUsage(usage) {
192
192
  "rejected_prediction_tokens",
193
193
  ].includes(k),
194
194
  )
195
- .map(([k, v]) => `${k}: ${v}`)
195
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
196
196
  .join(", ")}`,
197
197
  );
198
198
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @import { Message } from "./model"
3
3
  * @import { UserEventEmitter, AgentEventEmitter, AgentCommands } from "./agent"
4
- * @import { ClaudeCodePluginConfig } from "./config"
4
+ * @import { ClaudeCodePlugin } from "./claudeCodePlugin.mjs"
5
5
  */
6
6
 
7
7
  import { execFileSync } from "node:child_process";
@@ -197,7 +197,7 @@ const HELP_MESSAGE = [
197
197
  * @property {string} notifyCmd
198
198
  * @property {boolean} sandbox
199
199
  * @property {() => Promise<void>} onStop
200
- * @property {ClaudeCodePluginConfig[]} [claudeCodePlugins]
200
+ * @property {ClaudeCodePlugin[]} [claudeCodePlugins]
201
201
  */
202
202
 
203
203
  /**
@@ -257,7 +257,9 @@ export function startInteractiveSession({
257
257
  }
258
258
 
259
259
  const invocation = `${displayInvocation}${args ? ` ${args}` : ""}`;
260
- const message = `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
260
+ const message = prompt.isSkill
261
+ ? `System: This prompt was invoked as "${invocation}".\nPrompt path: ${prompt.filePath}\n\n${prompt.content}`
262
+ : `System: This prompt was invoked as "${invocation}".\n\n${prompt.content}`;
261
263
 
262
264
  console.log(styleText("gray", "\n<prompt>"));
263
265
  console.log(message);
package/src/config.d.ts CHANGED
@@ -3,11 +3,7 @@ import { ToolUsePattern } from "./tool";
3
3
  import { AskURLToolOptions } from "./tools/askURL.mjs";
4
4
  import { AskWebToolOptions } from "./tools/askWeb.mjs";
5
5
  import { ExecCommandSanboxConfig } from "./tools/execCommand";
6
-
7
- export type ClaudeCodePluginConfig = {
8
- name: string;
9
- path: string;
10
- };
6
+ import { ClaudeCodePluginRepo } from "./claudeCodePlugin.mjs";
11
7
 
12
8
  export type AppConfig = {
13
9
  model?: string;
@@ -25,7 +21,7 @@ export type AppConfig = {
25
21
  };
26
22
  mcpServers?: Record<string, MCPServerConfig>;
27
23
  notifyCmd?: string;
28
- claudeCodePlugins?: ClaudeCodePluginConfig[];
24
+ claudeCodePlugins?: ClaudeCodePluginRepo[];
29
25
  };
30
26
 
31
27
  export type MCPServerConfig = {
@@ -1,3 +1,5 @@
1
+ /** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
2
+
1
3
  import crypto from "node:crypto";
2
4
  import fs from "node:fs/promises";
3
5
  import path from "node:path";
@@ -7,7 +9,6 @@ import {
7
9
  AGENT_PROJECT_METADATA_DIR,
8
10
  AGENT_ROOT,
9
11
  AGENT_USER_CONFIG_DIR,
10
- CLAUDE_CODE_PLUGIN_DIR,
11
12
  } from "../env.mjs";
12
13
 
13
14
  /**
@@ -21,10 +22,11 @@ import {
21
22
 
22
23
  /**
23
24
  * Load all agent roles from the predefined directories.
24
- * @param {Array<{name: string, path: string}>} [claudeCodePlugins]
25
+ * @param {ClaudeCodePlugin[]} [claudeCodePlugins]
25
26
  * @returns {Promise<Map<string, AgentRole>>}
26
27
  */
27
28
  export async function loadAgentRoles(claudeCodePlugins) {
29
+ /** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
28
30
  const agentDirs = [
29
31
  {
30
32
  dir: path.resolve(AGENT_ROOT, ".config", "agents.predefined"),
@@ -41,11 +43,10 @@ export async function loadAgentRoles(claudeCodePlugins) {
41
43
  // Add plugin directories if provided
42
44
  if (claudeCodePlugins) {
43
45
  for (const plugin of claudeCodePlugins) {
44
- const pluginBase = path.join(CLAUDE_CODE_PLUGIN_DIR, plugin.path);
45
-
46
46
  agentDirs.push({
47
- dir: path.join(pluginBase, "agents"),
47
+ dir: path.join(plugin.path, "agents"),
48
48
  idPrefix: `claude/${plugin.name}:`,
49
+ only: plugin.only,
49
50
  });
50
51
  }
51
52
  }
@@ -53,7 +54,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
53
54
  /** @type {Map<string, AgentRole>} */
54
55
  const roles = new Map();
55
56
 
56
- for (const { dir, idPrefix } of agentDirs) {
57
+ for (const { dir, idPrefix, only } of agentDirs) {
57
58
  const files = await getMarkdownFiles(dir).catch((err) => {
58
59
  if (err.code !== "ENOENT") {
59
60
  console.warn(`Failed to list agent roles in ${dir}:`, err);
@@ -70,6 +71,11 @@ export async function loadAgentRoles(claudeCodePlugins) {
70
71
 
71
72
  if (content === null) continue;
72
73
 
74
+ // Filter by only pattern if specified
75
+ if (only && !only.test(file)) {
76
+ continue;
77
+ }
78
+
73
79
  let role = parseAgentRole(file, content, fullPath, idPrefix);
74
80
  if (role.import) {
75
81
  role = await mergeRemoteRole(role, file, fullPath);
@@ -1,3 +1,5 @@
1
+ /** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
2
+
1
3
  import { execFileSync } from "node:child_process";
2
4
  import crypto from "node:crypto";
3
5
  import fs from "node:fs/promises";
@@ -8,7 +10,6 @@ import {
8
10
  AGENT_PROJECT_METADATA_DIR,
9
11
  AGENT_ROOT,
10
12
  AGENT_USER_CONFIG_DIR,
11
- CLAUDE_CODE_PLUGIN_DIR,
12
13
  } from "../env.mjs";
13
14
 
14
15
  /**
@@ -25,10 +26,11 @@ import {
25
26
 
26
27
  /**
27
28
  * Load all prompts from the predefined directories.
28
- * @param {Array<{name: string, path: string}>} [claudeCodePlugins]
29
+ * @param {ClaudeCodePlugin[]} [claudeCodePlugins]
29
30
  * @returns {Promise<Map<string, Prompt>>}
30
31
  */
31
32
  export async function loadPrompts(claudeCodePlugins) {
33
+ /** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
32
34
  const promptDirs = [
33
35
  {
34
36
  dir: path.resolve(AGENT_ROOT, ".config", "prompts.predefined"),
@@ -49,18 +51,18 @@ export async function loadPrompts(claudeCodePlugins) {
49
51
  // Add plugin directories if provided
50
52
  if (claudeCodePlugins) {
51
53
  for (const plugin of claudeCodePlugins) {
52
- const pluginBase = path.join(CLAUDE_CODE_PLUGIN_DIR, plugin.path);
53
-
54
54
  // Commands
55
55
  promptDirs.push({
56
- dir: path.join(pluginBase, "commands"),
56
+ dir: path.join(plugin.path, "commands"),
57
57
  idPrefix: `claude/${plugin.name}/commands:`,
58
+ only: plugin.only,
58
59
  });
59
60
 
60
61
  // Skills
61
62
  promptDirs.push({
62
- dir: path.join(pluginBase, "skills"),
63
+ dir: path.join(plugin.path, "skills"),
63
64
  idPrefix: `claude/${plugin.name}/skills:`,
65
+ only: plugin.only,
64
66
  });
65
67
  }
66
68
  }
@@ -68,7 +70,7 @@ export async function loadPrompts(claudeCodePlugins) {
68
70
  /** @type {Map<string, Prompt>} */
69
71
  const prompts = new Map();
70
72
 
71
- for (const { dir, idPrefix } of promptDirs) {
73
+ for (const { dir, idPrefix, only } of promptDirs) {
72
74
  const files = await getMarkdownFiles(dir).catch((err) => {
73
75
  if (err.code !== "ENOENT") {
74
76
  console.warn(`Failed to list prompts in ${dir}:`, err);
@@ -85,6 +87,11 @@ export async function loadPrompts(claudeCodePlugins) {
85
87
 
86
88
  if (content === null) continue;
87
89
 
90
+ // Filter by only pattern if specified
91
+ if (only && !only.test(file)) {
92
+ continue;
93
+ }
94
+
88
95
  // Ignore all files in the skills/ directory except for SKILL.md.
89
96
  if (fullPath.match(/\/skills\//) && !file.endsWith("/SKILL.md")) {
90
97
  continue;
@@ -240,6 +247,7 @@ async function getMarkdownFiles(dir, baseDir = dir) {
240
247
  */
241
248
  function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
242
249
  const rawId = relativePath.replace(/\/SKILL\.md$/, "").replace(/\.md$/, "");
250
+ const isSkill = relativePath.endsWith("SKILL.md");
243
251
  const isShortcut = rawId.startsWith("shortcuts/");
244
252
  const id = isShortcut
245
253
  ? idPrefix + rawId.replace(/^shortcuts\//, "")
@@ -257,7 +265,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
257
265
  content: fileContent.trim(),
258
266
  filePath: fullPath,
259
267
  isShortcut,
260
- isSkill: relativePath.endsWith("SKILL.md"),
268
+ isSkill,
261
269
  };
262
270
  }
263
271
 
@@ -281,7 +289,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
281
289
  parseFrontmatterField(match[1], "user-invocable") === "true" ||
282
290
  undefined,
283
291
  isShortcut,
284
- isSkill: relativePath.endsWith("SKILL.md"),
292
+ isSkill,
285
293
  };
286
294
  }
287
295
  const userInvocable = frontmatter["user-invocable"];
@@ -304,7 +312,6 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
304
312
  * @param {string} field
305
313
  * @returns {string | undefined}
306
314
  */
307
-
308
315
  function parseFrontmatterField(frontmatter, field) {
309
316
  const regex = new RegExp(`^${field}:\\s*(.*)$`, "m");
310
317
  const match = frontmatter.match(regex);
package/src/main.mjs CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  import { styleText } from "node:util";
6
6
  import { createAgent } from "./agent.mjs";
7
+ import {
8
+ installClaudeCodePlugins,
9
+ resolvePluginPaths,
10
+ } from "./claudeCodePlugin.mjs";
7
11
  import { parseCliArgs, printHelp } from "./cliArgs.mjs";
8
12
  import { startBatchSession } from "./cliBatch.mjs";
9
13
  import { startInteractiveSession } from "./cliInteractive.mjs";
@@ -29,11 +33,11 @@ import { writeFileTool } from "./tools/writeFile.mjs";
29
33
  import { createToolUseApprover } from "./toolUseApprover.mjs";
30
34
 
31
35
  const cliArgs = parseCliArgs(process.argv);
32
- if (cliArgs.showHelp) {
36
+ if (cliArgs.subcommand.type === "help") {
33
37
  printHelp();
34
38
  }
35
39
 
36
- if (cliArgs.listModels) {
40
+ if (cliArgs.subcommand.type === "list-models") {
37
41
  const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
38
42
  if (!appConfig.models || appConfig.models.length === 0) {
39
43
  console.error("No models found in configuration.");
@@ -48,6 +52,11 @@ if (cliArgs.listModels) {
48
52
  process.exit(0);
49
53
  }
50
54
 
55
+ if (cliArgs.subcommand.type === "install-claude-code-plugins") {
56
+ await installClaudeCodePlugins();
57
+ process.exit(0);
58
+ }
59
+
51
60
  (async () => {
52
61
  const startTime = new Date();
53
62
  const sessionId = [
@@ -56,12 +65,18 @@ if (cliArgs.listModels) {
56
65
  `0${startTime.getMinutes()}`.slice(-2),
57
66
  ].join("-");
58
67
  const tmuxSessionId = `agent-${sessionId}`;
59
- const isBatchMode = Boolean(cliArgs.batch);
68
+
69
+ const isBatchMode = cliArgs.subcommand.type === "batch";
70
+ const configFiles =
71
+ cliArgs.subcommand.type === "batch" ||
72
+ cliArgs.subcommand.type === "interactive"
73
+ ? cliArgs.subcommand.config
74
+ : [];
60
75
 
61
76
  const { appConfig, loadedConfigPath } = await loadAppConfig({
62
77
  skipUserConfig: isBatchMode,
63
78
  skipTrustCheck: isBatchMode,
64
- configFiles: cliArgs.config,
79
+ configFiles,
65
80
  });
66
81
 
67
82
  // In batch mode, skip human-readable output
@@ -121,9 +136,17 @@ if (cliArgs.listModels) {
121
136
  }
122
137
  }
123
138
 
124
- const modelNameWithVariant = cliArgs.model || appConfig.model || "";
125
- const agentRoles = await loadAgentRoles(appConfig.claudeCodePlugins);
126
- const prompts = await loadPrompts(appConfig.claudeCodePlugins);
139
+ const modelFromConfig = appConfig.model || "";
140
+ const modelFromArgs =
141
+ cliArgs.subcommand.type === "batch" ||
142
+ cliArgs.subcommand.type === "interactive"
143
+ ? cliArgs.subcommand.model
144
+ : null;
145
+ const modelNameWithVariant = modelFromArgs || modelFromConfig;
146
+
147
+ const pluginPaths = resolvePluginPaths(appConfig.claudeCodePlugins ?? []);
148
+ const agentRoles = await loadAgentRoles(pluginPaths);
149
+ const prompts = await loadPrompts(pluginPaths);
127
150
 
128
151
  const prompt = createPrompt({
129
152
  username: USER_NAME,
@@ -216,19 +239,20 @@ if (cliArgs.listModels) {
216
239
  },
217
240
  };
218
241
 
219
- if (isBatchMode) {
220
- if (!cliArgs.batch) {
242
+ if (cliArgs.subcommand.type === "batch") {
243
+ const task = cliArgs.subcommand.task;
244
+ if (!task) {
221
245
  throw new Error("Batch task is required in batch mode");
222
246
  }
223
247
  await startBatchSession({
224
248
  ...sessionOptions,
225
- task: cliArgs.batch,
249
+ task,
226
250
  });
227
251
  } else {
228
252
  startInteractiveSession({
229
253
  ...sessionOptions,
230
254
  notifyCmd: appConfig.notifyCmd || AGENT_NOTIFY_CMD_DEFAULT,
231
- claudeCodePlugins: appConfig.claudeCodePlugins,
255
+ claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
232
256
  });
233
257
  }
234
258
  })().catch((err) => {