@ryan_nookpi/pi-extension-subagent 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonghak Seo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # subagent
2
+
3
+ Asynchronous subagent delegation for [pi](https://github.com/earendil-works/pi). Run specialist agents in dedicated child sessions, optionally pass selected main-session context, and receive results as follow-up messages.
4
+
5
+ > [!WARNING]
6
+ > Subagents run headlessly without approval prompts. Claude-runtime agents use permission bypass, and pi-runtime agents can use every tool listed in their agent definition. This extension is not a sandbox. Use it only in trusted repositories with trusted prompts and agent definitions.
7
+
8
+ ## Requirements
9
+
10
+ - pi 0.80.6 or later (tested with 0.80.6)
11
+ - For `runtime: claude` with the default `claudeRuntime: "sdk"`: supported Anthropic authentication such as `ANTHROPIC_API_KEY`; see the [official Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview)
12
+ - For `runtime: claude` with `claudeRuntime: "cli"`: the `claude` executable on `PATH` and an authenticated Claude Code installation
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pi install npm:@ryan_nookpi/pi-extension-subagent
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ ### 1. Create an agent
23
+
24
+ Agents are Markdown files with YAML frontmatter. Create `~/.pi/agent/agents/worker.md` for a global agent, or `.pi/agents/worker.md` inside one project:
25
+
26
+ ```markdown
27
+ ---
28
+ name: worker
29
+ description: Implements requested changes
30
+ model: anthropic/claude-sonnet-4-6
31
+ thinking: medium
32
+ tools: read,bash,edit,write
33
+ runtime: pi
34
+ ---
35
+
36
+ Implement the requested changes and verify them.
37
+ ```
38
+
39
+ `name` and `description` are required. Optional fields are:
40
+
41
+ - `runtime`: `pi` (default) or `claude`
42
+ - `model`: runtime-compatible model ID
43
+ - `thinking`: `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`
44
+ - `tools`: comma-separated tool names
45
+
46
+ Omitted model, thinking, and tools values use that runtime's defaults.
47
+
48
+ ### 2. Confirm discovery
49
+
50
+ From the interactive pi UI:
51
+
52
+ ```text
53
+ /subagents
54
+ ```
55
+
56
+ From an AI tool call, pass a command string to the `subagent` tool:
57
+
58
+ ```json
59
+ { "command": "subagent agents" }
60
+ ```
61
+
62
+ The `subagent ...` examples in this README are **tool command strings**, not terminal commands. Do not run them in Bash.
63
+
64
+ ### 3. Launch a run
65
+
66
+ Interactive user command:
67
+
68
+ ```text
69
+ /sub:isolate worker implement the requested change and run tests
70
+ ```
71
+
72
+ Equivalent AI tool call:
73
+
74
+ ```json
75
+ { "command": "subagent run worker --isolated -- implement the requested change and run tests" }
76
+ ```
77
+
78
+ Runs are asynchronous in interactive mode. Wait for the automatic completion or failure follow-up instead of immediately polling `status` or `detail`.
79
+
80
+ ## Agent discovery
81
+
82
+ Definitions are loaded from the following locations. Later sources override earlier agents with the same name:
83
+
84
+ 1. `$PI_CODING_AGENT_DIR/agents/*.md` (normally `~/.pi/agent/agents/*.md`)
85
+ 2. Nearest `.claude/agents/**/*.md`
86
+ 3. Nearest `.pi/agents/*.md`
87
+
88
+ Project `.claude/agents` files are discovered recursively. Project `.pi/agents` files are discovered only in the selected directory.
89
+
90
+ ## Context modes and lifecycle
91
+
92
+ - `--isolated` starts a dedicated child session without copying the main conversation. It is the default for `subagent` tool launches.
93
+ - `--main` adds selected main-session context to the child task.
94
+ - `/sub:isolate` selects isolated context; `/sub:main` selects main context.
95
+ - `>>` and `>` use main-session context.
96
+ - Continuing a run preserves its original context mode and child session. Supplying `--main` or `--isolated` to `continue` does not retroactively change it.
97
+
98
+ Pi replaces and invalidates extension runtimes during `/new`, `/resume`, `/fork`, and reload. Active child processes are therefore aborted during `session_shutdown`, and the old session records why they stopped. Wait for active runs before replacing the parent session. This follows pi's [official extension lifecycle guidance](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#long-lived-resources-and-shutdown).
99
+
100
+ ## Tool interface
101
+
102
+ The extension registers two main-session tools:
103
+
104
+ - `list-agents`: return discovered agent definitions and runtime settings
105
+ - `subagent`: accept one CLI-style command string
106
+
107
+ ```text
108
+ subagent help
109
+ subagent agents
110
+ subagent runs
111
+ subagent run <agent> [--main|--isolated] -- <task>
112
+ subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>
113
+ subagent batch [--main|--isolated] --agent <agent> --task <task> [--agent <agent> --task <task> ...]
114
+ subagent chain [--main|--isolated] --agent <agent> --task <task> [--agent <agent> --task <task> ...]
115
+ subagent status <runId>
116
+ subagent detail <runId>
117
+ subagent abort <runId|runId,runId|all>
118
+ subagent remove <runId|runId,runId|all>
119
+ ```
120
+
121
+ `batch` runs independent tasks in parallel. Quote tasks containing spaces:
122
+
123
+ ```json
124
+ {
125
+ "command": "subagent batch --main --agent worker --task \"implement feature A\" --agent reviewer --task \"review feature B\""
126
+ }
127
+ ```
128
+
129
+ `chain` runs steps sequentially and gives each step the previous result as reference:
130
+
131
+ ```json
132
+ {
133
+ "command": "subagent chain --isolated --agent worker --task \"implement the change\" --agent reviewer --task \"review the implementation\""
134
+ }
135
+ ```
136
+
137
+ Use `status` and `detail` only for explicit, one-off inspection. Repeated polling is unnecessary because completion is delivered automatically.
138
+
139
+ ## Slash commands
140
+
141
+ - `/subagents` — list discovered agents and settings
142
+ - `/sub:main [agent|alias|runId] <task>` — launch with main-session context or continue a run
143
+ - `/sub:isolate [agent|alias|runId] <task>` — launch in isolated context or continue a run
144
+ - `/sub:peek [runId]` — show the latest response; defaults to the latest run
145
+ - `/sub:open [runId]` — open session replay; defaults to the latest run
146
+ - `/sub:history` — show all run history, including removed runs
147
+ - `/sub:rm [runId]` — remove a run; defaults to the latest and aborts it if necessary
148
+ - `/sub:clear [all]` — clear finished runs, or every run with `all`
149
+ - `/sub:abort [runId|all]` — abort the latest running run, one run, or all running runs
150
+
151
+ When an agent is omitted, launch commands use `defaultAgent`.
152
+
153
+ ## Interactive shortcuts
154
+
155
+ | Shortcut | Behavior |
156
+ | --- | --- |
157
+ | `>> [agent\|runId] <task>` | Visible run using main-session context |
158
+ | `> [agent\|runId] <task>` | Hidden run using main-session context; interactive UI only |
159
+ | `#<runId> <task>` | Continue a run |
160
+ | `>><symbol> <task>` | Visible run using the agent mapped in `symbolMap` |
161
+ | `><symbol> <task>` | Hidden run using the mapped agent |
162
+ | `<>runId` | Compact form of `/sub:peek runId` |
163
+ | `<< [runId\|runId,runId]` | Abort selected running runs or clear selected finished runs; without arguments, abort the latest running run |
164
+ | `<<< [all]` | Clear finished runs; use `all` to clear every run |
165
+
166
+ Hidden runs do not add start or completion messages to the main transcript. Read their output with `/sub:peek`, `<>runId`, or `/sub:open`. A plain `>` shortcut requires a space before its task; configured symbol shortcuts do not.
167
+
168
+ ## Escalation from pi-runtime agents
169
+
170
+ Pi-runtime subagent sessions receive an `ask_master` tool. It lets a child report a decision that the parent must make, then immediately terminates that child run. The parent receives the escalation as a follow-up.
171
+
172
+ Use `ask_master` only when the child cannot safely proceed, such as before a destructive operation or an unresolved architecture decision. Claude-runtime agents do not receive this tool; they report blockers in their final text instead.
173
+
174
+ ## Configuration
175
+
176
+ Global configuration belongs under `subagent` in `$PI_CODING_AGENT_DIR/settings.json` (normally `~/.pi/agent/settings.json`):
177
+
178
+ ```json
179
+ {
180
+ "subagent": {
181
+ "claudeRuntime": "sdk",
182
+ "defaultAgent": "worker",
183
+ "symbolMap": {
184
+ "?": "searcher",
185
+ "!": "reviewer"
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ A nearest project `.pi/subagent.json` overrides global values:
192
+
193
+ ```json
194
+ {
195
+ "defaultAgent": "worker",
196
+ "symbolMap": {
197
+ "?": "searcher"
198
+ }
199
+ }
200
+ ```
201
+
202
+ - `claudeRuntime`: `sdk` (default) or `cli`; applies only to agents with `runtime: claude`
203
+ - `defaultAgent`: agent used when a launch omits its agent; defaults to `worker` and must match a discovered definition
204
+ - `symbolMap`: one-character shortcuts mapped to non-empty agent names; a valid project map replaces the global map as a whole, while a malformed project map falls back to the valid global map
205
+
206
+ ### Context guard override
207
+
208
+ `PI_SUBAGENT_CONTEXT_GUARD_TOKENS` overrides the proactive context limit for pi-runtime children. Set it to a positive integer to apply that ceiling to every pi model. Set it to `0` or an empty value to disable the proactive guard and rely on native compaction or provider overflow handling.
209
+
210
+ ## Troubleshooting
211
+
212
+ ### `Configured defaultAgent "worker" was not found`
213
+
214
+ Create a `worker` definition from the quick start, choose an existing agent explicitly, or change `defaultAgent`. Run `/subagents` to verify discovery before launching.
215
+
216
+ ### Claude SDK authentication failure
217
+
218
+ Confirm the environment used to start pi has valid Anthropic authentication, such as `ANTHROPIC_API_KEY`. The SDK runtime does not require the Claude Code CLI.
219
+
220
+ ### `spawn claude ENOENT`
221
+
222
+ `claudeRuntime` is set to `cli`, but the `claude` executable is not on `PATH`. Install and authenticate Claude Code, or switch back to `claudeRuntime: "sdk"`.
223
+
224
+ ### Hidden shortcut produces no transcript message
225
+
226
+ That is intentional. Hidden `>` runs are human-only UI jobs. Inspect them with `/sub:peek`, `<>runId`, or `/sub:open`.
227
+
228
+ ## Security and trust boundary
229
+
230
+ - Claude SDK execution uses `permissionMode: "bypassPermissions"` with `allowDangerouslySkipPermissions`; Claude CLI execution uses `--dangerously-skip-permissions`.
231
+ - Pi-runtime children are headless and have unrestricted access to the tools declared by their agent.
232
+ - Project agent definitions are repository-controlled instructions. Review `.pi/agents` and `.claude/agents` before running this extension in an unfamiliar repository.
233
+ - Restrict each agent's `tools` list to what it needs. Avoid broad shell or write access for read-only review agents.
234
+ - `--isolated` separates conversation context; it does not provide filesystem, process, credential, or network isolation.
235
+
236
+ ## Stability
237
+
238
+ This is a `0.1.x` release. The commands, configuration keys, agent frontmatter, and behaviors documented here are the supported surface. Internal TypeScript modules included in the npm tarball are implementation details and may change during the `0.x` series. Compatibility is currently tested against pi 0.80.6.
239
+
240
+ ## License
241
+
242
+ MIT
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Legacy above-editor pixel subagent widget is disabled.
3
+ *
4
+ * All subagent runs are rendered by widget.ts in the above-editor run status widget regardless of launch source.
5
+ */
6
+
7
+ import type { SubagentStore } from "./store.js";
8
+ import type { WidgetRenderCtx } from "./widget.js";
9
+
10
+ export function updatePixelWidget(store: SubagentStore, ctx?: Pick<WidgetRenderCtx, "hasUI" | "ui"> | null): void {
11
+ const activeCtx = ctx ?? store.pixelWidgetCtx;
12
+ if (!activeCtx?.hasUI) return;
13
+ store.pixelWidgetCtx = activeCtx;
14
+ activeCtx.ui?.setWidget("pixel-subagents", undefined);
15
+ }
16
+
17
+ export function cleanupPixelTimer(): void {
18
+ // no-op: legacy pixel widget is disabled
19
+ return;
20
+ }
package/agents.ts ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Agent discovery and configuration
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ AGENT_THINKING_LEVELS,
10
+ type AgentThinkingLevel,
11
+ normalizeModel,
12
+ normalizeThinkingLevel,
13
+ normalizeTools,
14
+ } from "./utils/agent-utils.js";
15
+
16
+ export const THINKING_LEVELS = AGENT_THINKING_LEVELS;
17
+
18
+ export type AgentRuntime = "pi" | "claude";
19
+
20
+ export interface AgentConfig {
21
+ name: string;
22
+ description: string;
23
+ tools?: string[];
24
+ model?: string;
25
+ thinking?: AgentThinkingLevel;
26
+ systemPrompt: string;
27
+ source: "user" | "project";
28
+ filePath: string;
29
+ runtime: AgentRuntime;
30
+ }
31
+
32
+ export interface AgentDiscoveryResult {
33
+ agents: AgentConfig[];
34
+ projectAgentsDir: string | null;
35
+ }
36
+
37
+ interface LoadAgentsOptions {
38
+ recursive?: boolean;
39
+ format?: "pi" | "claude";
40
+ }
41
+
42
+ const COMMON_SUBAGENT_NO_RECURSION_RULE = [
43
+ "Global Runtime Rule (subagent):",
44
+ "- Never invoke the `subagent` tool.",
45
+ "- Never trigger subagent commands/shorthands such as `/sub:*`, `>>`, or `>`.",
46
+ "- If delegation is requested, explain that recursive subagent invocation is disabled and continue with available tools.",
47
+ ].join("\n");
48
+
49
+ const COMMON_SUBAGENT_ESCALATION_GUIDELINE = [
50
+ "ask_master Guideline:",
51
+ "- The `ask_master` tool asks the master for a decision. WARNING: calling it terminates your session immediately.",
52
+ "- Use `ask_master` when:",
53
+ " - You encounter ambiguity that cannot be resolved from the task context or codebase",
54
+ " - A decision has significant impact (deletion, architecture change, deployment) and you are unsure of the correct choice",
55
+ " - You discover unexpected issues that fundamentally change the scope of the task",
56
+ " - Task instructions conflict with each other and you need clarification",
57
+ "- DO NOT use `ask_master` for:",
58
+ " - Routine decisions within your domain expertise",
59
+ " - Issues you can resolve with available tools and context",
60
+ " - Minor style, formatting, or naming choices",
61
+ " - Pre-existing problems unrelated to the current task",
62
+ "- When calling, always include:",
63
+ " - Clear description of the blocker or decision needed",
64
+ " - Options you have considered with pros/cons",
65
+ " - Your recommendation, if you have one",
66
+ ].join("\n");
67
+
68
+ const COMMON_CLAUDE_RUNTIME_ESCALATION_GUIDELINE = [
69
+ "Blocker Reporting Guideline:",
70
+ "- If you encounter a blocker that you cannot resolve with available tools and context,",
71
+ " report it as plain text at the end of your response.",
72
+ "- Do NOT attempt to call tools that are not available in your environment (e.g. ask_master).",
73
+ "- Include: a clear description of the blocker, options you considered, and your recommendation.",
74
+ ].join("\n");
75
+
76
+ function attachCommonSubagentRule(systemPrompt: string, runtime: AgentRuntime = "pi"): string {
77
+ let prompt = systemPrompt.trimEnd();
78
+ if (!prompt.includes("Global Runtime Rule (subagent):")) {
79
+ prompt = prompt ? `${prompt}\n\n${COMMON_SUBAGENT_NO_RECURSION_RULE}` : COMMON_SUBAGENT_NO_RECURSION_RULE;
80
+ }
81
+ if (runtime === "claude") {
82
+ if (!prompt.includes("Blocker Reporting Guideline:")) {
83
+ prompt = prompt
84
+ ? `${prompt}\n\n${COMMON_CLAUDE_RUNTIME_ESCALATION_GUIDELINE}`
85
+ : COMMON_CLAUDE_RUNTIME_ESCALATION_GUIDELINE;
86
+ }
87
+ } else {
88
+ if (!prompt.includes("ask_master Guideline:")) {
89
+ prompt = prompt ? `${prompt}\n\n${COMMON_SUBAGENT_ESCALATION_GUIDELINE}` : COMMON_SUBAGENT_ESCALATION_GUIDELINE;
90
+ }
91
+ }
92
+ return prompt;
93
+ }
94
+
95
+ function listMarkdownFiles(dir: string, recursive: boolean): string[] {
96
+ const files: string[] = [];
97
+ const stack: string[] = [dir];
98
+
99
+ while (stack.length > 0) {
100
+ const currentDir = stack.pop() as string;
101
+ let entries: fs.Dirent[];
102
+ try {
103
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
104
+ } catch {
105
+ continue;
106
+ }
107
+
108
+ for (const entry of entries) {
109
+ const fullPath = path.join(currentDir, entry.name);
110
+ if (entry.isDirectory()) {
111
+ if (recursive) stack.push(fullPath);
112
+ continue;
113
+ }
114
+
115
+ if (!entry.name.endsWith(".md")) continue;
116
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
117
+ files.push(fullPath);
118
+ }
119
+ }
120
+
121
+ files.sort((a, b) => a.localeCompare(b));
122
+ return files;
123
+ }
124
+
125
+ function loadAgentsFromDir(dir: string, source: "user" | "project", options: LoadAgentsOptions = {}): AgentConfig[] {
126
+ const agents: AgentConfig[] = [];
127
+ const recursive = options.recursive ?? false;
128
+ const format = options.format ?? "pi";
129
+
130
+ if (!fs.existsSync(dir)) {
131
+ return agents;
132
+ }
133
+
134
+ const files = listMarkdownFiles(dir, recursive);
135
+ for (const filePath of files) {
136
+ let content: string;
137
+ try {
138
+ content = fs.readFileSync(filePath, "utf-8");
139
+ } catch {
140
+ continue;
141
+ }
142
+
143
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
144
+ if (!frontmatter.name || !frontmatter.description) continue;
145
+
146
+ const tools = normalizeTools(frontmatter.tools, format);
147
+ const model = normalizeModel(frontmatter.model, format);
148
+ const thinking = normalizeThinkingLevel(frontmatter.thinking);
149
+ const runtime: AgentRuntime = frontmatter.runtime === "claude" ? "claude" : "pi";
150
+
151
+ agents.push({
152
+ name: frontmatter.name,
153
+ description: frontmatter.description,
154
+ tools,
155
+ model,
156
+ thinking,
157
+ systemPrompt: attachCommonSubagentRule(body, runtime),
158
+ source,
159
+ filePath,
160
+ runtime,
161
+ });
162
+ }
163
+
164
+ return agents;
165
+ }
166
+
167
+ function isDirectory(p: string): boolean {
168
+ try {
169
+ return fs.statSync(p).isDirectory();
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ function findNearestProjectAgentsDir(cwd: string): string | null {
176
+ let currentDir = cwd;
177
+ while (true) {
178
+ const candidate = path.join(currentDir, ".pi", "agents");
179
+ if (isDirectory(candidate)) return candidate;
180
+
181
+ const parentDir = path.dirname(currentDir);
182
+ if (parentDir === currentDir) return null;
183
+ currentDir = parentDir;
184
+ }
185
+ }
186
+
187
+ function findNearestClaudeAgentsDir(cwd: string): string | null {
188
+ let currentDir = cwd;
189
+ while (true) {
190
+ const candidate = path.join(currentDir, ".claude", "agents");
191
+ if (isDirectory(candidate)) return candidate;
192
+
193
+ const parentDir = path.dirname(currentDir);
194
+ if (parentDir === currentDir) return null;
195
+ currentDir = parentDir;
196
+ }
197
+ }
198
+
199
+ export function discoverAgents(cwd: string): AgentDiscoveryResult {
200
+ const userDir = path.join(getAgentDir(), "agents");
201
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
202
+ const claudeAgentsDir = findNearestClaudeAgentsDir(cwd);
203
+
204
+ const userAgents = loadAgentsFromDir(userDir, "user", { format: "pi" });
205
+ const projectPiAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project", { format: "pi" }) : [];
206
+ const projectClaudeAgents = claudeAgentsDir
207
+ ? loadAgentsFromDir(claudeAgentsDir, "project", { format: "claude", recursive: true })
208
+ : [];
209
+
210
+ // Priority: user < .claude/agents < .pi/agents
211
+ const projectAgents = [...projectClaudeAgents, ...projectPiAgents];
212
+
213
+ const agentMap = new Map<string, AgentConfig>();
214
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
215
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
216
+
217
+ const projectSources = [projectAgentsDir, claudeAgentsDir].filter((dir): dir is string => Boolean(dir));
218
+
219
+ return {
220
+ agents: Array.from(agentMap.values()),
221
+ projectAgentsDir: projectSources.length > 0 ? projectSources.join(", ") : null,
222
+ };
223
+ }
224
+
225
+ export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
226
+ if (agents.length === 0) return { text: "none", remaining: 0 };
227
+ const listed = agents.slice(0, maxItems);
228
+ const remaining = agents.length - listed.length;
229
+ return {
230
+ text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
231
+ remaining,
232
+ };
233
+ }
package/claude-args.ts ADDED
@@ -0,0 +1,103 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { mapPiToolsToClaude } from "./utils/agent-utils.js";
4
+
5
+ export interface ClaudeArgsConfig {
6
+ prompt: string;
7
+ tools: string[];
8
+ model?: string;
9
+ thinking?: string;
10
+ resumeSessionId?: string;
11
+ cwd?: string;
12
+ mcpConfigPath?: string;
13
+ systemPromptFile?: string;
14
+ }
15
+
16
+ const MCP_CONFIG_CANDIDATES = [".mcp.json"] as const;
17
+
18
+ export function mapThinkingToClaudeEffort(piThinking: string): "low" | "medium" | "high" | "max" | undefined {
19
+ switch (piThinking) {
20
+ case "off":
21
+ case "minimal":
22
+ return "low";
23
+ case "low":
24
+ return "medium";
25
+ case "medium":
26
+ return "high";
27
+ case "high":
28
+ case "xhigh":
29
+ return "max";
30
+ case "max":
31
+ return "max";
32
+ default:
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ export function findProjectMcpConfig(cwd: string): string | undefined {
38
+ let currentDir = path.resolve(cwd);
39
+
40
+ while (true) {
41
+ for (const candidate of MCP_CONFIG_CANDIDATES) {
42
+ const fullPath = path.join(currentDir, candidate);
43
+ try {
44
+ if (fs.statSync(fullPath).isFile()) return fullPath;
45
+ } catch {}
46
+ }
47
+
48
+ const parentDir = path.dirname(currentDir);
49
+ if (parentDir === currentDir) return undefined;
50
+ currentDir = parentDir;
51
+ }
52
+ }
53
+
54
+ export function buildClaudeArgs(config: ClaudeArgsConfig): string[] {
55
+ const args: string[] = [
56
+ "-p",
57
+ "--output-format",
58
+ "stream-json",
59
+ "--include-partial-messages",
60
+ "--verbose",
61
+ "--dangerously-skip-permissions",
62
+ ];
63
+
64
+ const claudeTools = mapPiToolsToClaude(config.tools);
65
+
66
+ if (claudeTools.length > 0) {
67
+ args.push("--tools", claudeTools.join(","));
68
+ args.push("--allowedTools", claudeTools.join(","));
69
+ }
70
+
71
+ args.push("--strict-mcp-config");
72
+
73
+ if (config.mcpConfigPath) {
74
+ args.push("--mcp-config", config.mcpConfigPath);
75
+ } else if (config.cwd) {
76
+ const discovered = findProjectMcpConfig(config.cwd);
77
+ if (discovered) {
78
+ args.push("--mcp-config", discovered);
79
+ }
80
+ }
81
+
82
+ if (config.model) {
83
+ const normalized = config.model.replace(/^anthropic\//, "");
84
+ args.push("--model", normalized);
85
+ }
86
+
87
+ if (config.thinking) {
88
+ const effort = mapThinkingToClaudeEffort(config.thinking);
89
+ if (effort) args.push("--effort", effort);
90
+ }
91
+
92
+ if (config.resumeSessionId) {
93
+ args.push("--resume", config.resumeSessionId);
94
+ }
95
+
96
+ if (config.systemPromptFile) {
97
+ args.push("--append-system-prompt-file", config.systemPromptFile);
98
+ }
99
+
100
+ args.push(config.prompt);
101
+
102
+ return args;
103
+ }