@meowlynxsea/koi 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.
Files changed (109) hide show
  1. package/LICENSE +34 -0
  2. package/NOTICE +35 -0
  3. package/README.md +15 -0
  4. package/bin/koi +12 -0
  5. package/dist/highlights-eq9cgrbb.scm +604 -0
  6. package/dist/highlights-ghv9g403.scm +205 -0
  7. package/dist/highlights-hk7bwhj4.scm +284 -0
  8. package/dist/highlights-r812a2qc.scm +150 -0
  9. package/dist/highlights-x6tmsnaa.scm +115 -0
  10. package/dist/injections-73j83es3.scm +27 -0
  11. package/dist/main.js +489918 -0
  12. package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  13. package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
  14. package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  15. package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  16. package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
  17. package/package.json +51 -0
  18. package/src/agent/check-permissions.ts +239 -0
  19. package/src/agent/hooks/message-utils.ts +305 -0
  20. package/src/agent/hooks/types.ts +32 -0
  21. package/src/agent/hooks.ts +1560 -0
  22. package/src/agent/mode.ts +163 -0
  23. package/src/agent/monitor-registry.ts +308 -0
  24. package/src/agent/permission-ui.ts +71 -0
  25. package/src/agent/plan-ui.ts +74 -0
  26. package/src/agent/question-ui.ts +58 -0
  27. package/src/agent/session-fork.ts +299 -0
  28. package/src/agent/session-snapshots.ts +216 -0
  29. package/src/agent/session-store.ts +649 -0
  30. package/src/agent/session-tasks.ts +305 -0
  31. package/src/agent/session.ts +27 -0
  32. package/src/agent/subagent-registry.ts +176 -0
  33. package/src/agent/subagent.ts +194 -0
  34. package/src/agent/tool-orchestration.ts +55 -0
  35. package/src/agent/tools.ts +8 -0
  36. package/src/cli/args.ts +6 -0
  37. package/src/cli/commands.ts +5 -0
  38. package/src/commands/skills/index.ts +23 -0
  39. package/src/config/models.ts +6 -0
  40. package/src/config/settings.ts +392 -0
  41. package/src/main.tsx +64 -0
  42. package/src/services/mcp/client.ts +194 -0
  43. package/src/services/mcp/config.ts +232 -0
  44. package/src/services/mcp/connection-manager.ts +258 -0
  45. package/src/services/mcp/index.ts +80 -0
  46. package/src/services/mcp/mcp-commands.ts +114 -0
  47. package/src/services/mcp/stdio-transport.ts +246 -0
  48. package/src/services/mcp/types.ts +155 -0
  49. package/src/skills/SkillsMenu.tsx +370 -0
  50. package/src/skills/bundled/batch.ts +106 -0
  51. package/src/skills/bundled/debug.ts +86 -0
  52. package/src/skills/bundled/loremIpsum.ts +101 -0
  53. package/src/skills/bundled/remember.ts +97 -0
  54. package/src/skills/bundled/simplify.ts +100 -0
  55. package/src/skills/bundled/skillify.ts +123 -0
  56. package/src/skills/bundled/stuck.ts +101 -0
  57. package/src/skills/bundled/updateConfig.ts +228 -0
  58. package/src/skills/bundled.ts +46 -0
  59. package/src/skills/frontmatter.ts +179 -0
  60. package/src/skills/index.ts +87 -0
  61. package/src/skills/invoke.ts +231 -0
  62. package/src/skills/loader.ts +710 -0
  63. package/src/skills/substitution.ts +169 -0
  64. package/src/skills/types.ts +201 -0
  65. package/src/tools/agent.ts +143 -0
  66. package/src/tools/ask-user-question.ts +46 -0
  67. package/src/tools/bash.ts +148 -0
  68. package/src/tools/edit.ts +164 -0
  69. package/src/tools/glob.ts +102 -0
  70. package/src/tools/grep.ts +248 -0
  71. package/src/tools/index.ts +73 -0
  72. package/src/tools/list-mcp-resources.ts +74 -0
  73. package/src/tools/ls.ts +85 -0
  74. package/src/tools/mcp.ts +76 -0
  75. package/src/tools/monitor.ts +159 -0
  76. package/src/tools/plan-mode.ts +134 -0
  77. package/src/tools/read-mcp-resource.ts +79 -0
  78. package/src/tools/read.ts +137 -0
  79. package/src/tools/skill.ts +176 -0
  80. package/src/tools/task.ts +349 -0
  81. package/src/tools/types.ts +52 -0
  82. package/src/tools/webfetch-domains.ts +239 -0
  83. package/src/tools/webfetch.ts +533 -0
  84. package/src/tools/write.ts +101 -0
  85. package/src/tui/app.tsx +1178 -0
  86. package/src/tui/components/chat-panel.tsx +1071 -0
  87. package/src/tui/components/command-panel.tsx +261 -0
  88. package/src/tui/components/confirm-modal.tsx +135 -0
  89. package/src/tui/components/connect-modal.tsx +435 -0
  90. package/src/tui/components/connecting-modal.tsx +167 -0
  91. package/src/tui/components/edit-pending-modal.tsx +103 -0
  92. package/src/tui/components/exit-modal.tsx +131 -0
  93. package/src/tui/components/fork-modal.tsx +377 -0
  94. package/src/tui/components/image-preview-modal.tsx +141 -0
  95. package/src/tui/components/image-utils.ts +128 -0
  96. package/src/tui/components/info-bar.tsx +103 -0
  97. package/src/tui/components/input-box.tsx +352 -0
  98. package/src/tui/components/mcp/MCPSettings.tsx +386 -0
  99. package/src/tui/components/mcp/index.ts +7 -0
  100. package/src/tui/components/model-modal.tsx +310 -0
  101. package/src/tui/components/pending-area.tsx +88 -0
  102. package/src/tui/components/rename-modal.tsx +119 -0
  103. package/src/tui/components/session-modal.tsx +233 -0
  104. package/src/tui/components/side-bar.tsx +349 -0
  105. package/src/tui/components/tool-output.ts +6 -0
  106. package/src/tui/hooks/user-prompt-history.ts +114 -0
  107. package/src/tui/theme.ts +63 -0
  108. package/src/types/commands.ts +80 -0
  109. package/src/types/cross-spawn.d.ts +24 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Argument Substitution for Skills
3
+ *
4
+ * Handles {{skill.args}} and <arg> placeholders in skill prompts.
5
+ */
6
+
7
+ import type { ToolUseContext } from "./types.js";
8
+
9
+ /**
10
+ * Substitute arguments in skill content
11
+ * Supports {{skill.args}}, {{skill.arg.name}}, and <name> placeholders
12
+ */
13
+ export function substituteArguments(
14
+ content: string,
15
+ args: string,
16
+ strict: boolean,
17
+ argNames?: string[]
18
+ ): string {
19
+ let result = content;
20
+
21
+ // Replace {{skill.args}} with the full argument string
22
+ result = result.replace(/\{\{skill\.args\}\}/g, args);
23
+
24
+ // If argNames are provided, try to parse named arguments
25
+ if (argNames && argNames.length > 0) {
26
+ const parsedArgs = parseNamedArguments(args, argNames);
27
+
28
+ // Replace {{skill.arg.name}} patterns
29
+ result = result.replace(/\{\{skill\.arg\.(\w+)\}\}/g, (match, name) => {
30
+ const value = parsedArgs[name as keyof typeof parsedArgs];
31
+ return value ?? (strict ? match : "");
32
+ });
33
+
34
+ // Replace <name> patterns with values
35
+ for (const [name, value] of Object.entries(parsedArgs)) {
36
+ result = result.replace(new RegExp(`<${name}>`, "g"), value);
37
+ }
38
+ }
39
+
40
+ // Replace remaining <...> placeholders with args (for backward compatibility)
41
+ result = result.replace(/<[^>]+>/g, (match) => {
42
+ if (match === args) return args; // If the whole thing is the args, keep it
43
+ return match; // Leave other placeholders as-is
44
+ });
45
+
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * Parse named arguments from a string
51
+ * e.g., "file.ts --flag value" with argNames ["file", "flag"]
52
+ * -> { file: "file.ts", flag: "value" }
53
+ */
54
+ export function parseNamedArguments(args: string, argNames: string[]): Record<string, string> {
55
+ const result: Record<string, string> = {};
56
+ const parts = args.trim().split(/\s+/).filter(Boolean);
57
+
58
+ let partIndex = 0;
59
+ for (const name of argNames) {
60
+ if (partIndex >= parts.length) {
61
+ result[name] = "";
62
+ continue;
63
+ }
64
+
65
+ const part = parts[partIndex]!;
66
+
67
+ // Check if this part looks like a named argument (--name or --name=value)
68
+ if (part.startsWith("--")) {
69
+ const eqIdx = part.indexOf("=");
70
+ if (eqIdx !== -1) {
71
+ // --name=value format
72
+ const argName = part.slice(2, eqIdx);
73
+ const argValue = part.slice(eqIdx + 1);
74
+ result[argName] = argValue;
75
+ partIndex++;
76
+ } else {
77
+ // --name value format
78
+ result[name] = parts[partIndex + 1] ?? "";
79
+ partIndex += 2;
80
+ }
81
+ } else if (part.startsWith("-")) {
82
+ // Single dash flag
83
+ result[name] = part;
84
+ partIndex++;
85
+ } else {
86
+ // Positional argument
87
+ result[name] = part;
88
+ partIndex++;
89
+ }
90
+ }
91
+
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Parse argument names from arguments frontmatter
97
+ * e.g., "<file> <target>" -> ["file", "target"]
98
+ */
99
+ export function parseArgumentNames(argumentsFrontmatter?: string | string[]): string[] {
100
+ if (!argumentsFrontmatter) return [];
101
+
102
+ const args = Array.isArray(argumentsFrontmatter)
103
+ ? argumentsFrontmatter
104
+ : argumentsFrontmatter.split(/\s+/);
105
+
106
+ return args.map(arg => arg.replace(/[<>]/g, "").trim()).filter(Boolean);
107
+ }
108
+
109
+ /**
110
+ * Execute shell commands in skill prompt
111
+ * Supports !`command` and ```! ... ``` syntax
112
+ */
113
+ export async function executeShellCommandsInPrompt(
114
+ content: string,
115
+ _ctx: ToolUseContext,
116
+ skillName: string
117
+ ): Promise<string> {
118
+ // For now, we'll just strip shell commands as execution requires careful handling
119
+ // This is a simplified version - full implementation would execute and replace
120
+
121
+ let result = content;
122
+
123
+ // Remove inline shell commands: !`command`
124
+ result = result.replace(/!`([^`]+)`/g, (_, cmd) => {
125
+ // In a full implementation, this would execute the command
126
+ // and replace the !`...` with the output
127
+ console.log(`[skill:${skillName}] Would execute: ${cmd}`);
128
+ return `[shell output placeholder: ${cmd}]`;
129
+ });
130
+
131
+ // Remove block shell commands: ```! ... ```
132
+ result = result.replace(/```!([\s\S]*?)```/g, (_, cmd: string) => {
133
+ const trimmedCmd = cmd.trim();
134
+ console.log(`[skill:${skillName}] Would execute block: ${trimmedCmd}`);
135
+ return `[shell output placeholder]`;
136
+ });
137
+
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Substitute environment variables in content
143
+ */
144
+ export function substituteEnvVariables(
145
+ content: string,
146
+ env: Record<string, unknown>,
147
+ sessionId?: string,
148
+ skillDir?: string
149
+ ): string {
150
+ let result = content;
151
+
152
+ // Replace ${CLAUDE_SKILL_DIR}
153
+ if (skillDir) {
154
+ result = result.replace(/\$\{CLAUDE_SKILL_DIR\}/g, skillDir);
155
+ }
156
+
157
+ // Replace ${CLAUDE_SESSION_ID}
158
+ if (sessionId) {
159
+ result = result.replace(/\$\{CLAUDE_SESSION_ID\}/g, sessionId);
160
+ }
161
+
162
+ // Replace ${ENV:VAR_NAME}
163
+ result = result.replace(/\$\{ENV:(\w+)\}/g, (_, varName) => {
164
+ const value = env[varName as keyof typeof env];
165
+ return value !== undefined ? String(value) : "";
166
+ });
167
+
168
+ return result;
169
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Skills System Type Definitions
3
+ *
4
+ * Defines the core types for Koi's Skills functionality.
5
+ * Skills are reusable prompt templates with metadata that can be invoked
6
+ * via slash commands (e.g., /review, /test).
7
+ */
8
+
9
+ /**
10
+ * Content block type for skill prompts.
11
+ * Simplified version of the AI SDK's ContentBlockParam type.
12
+ */
13
+ export type ContentBlockParam =
14
+ | { type: "text"; text: string }
15
+ | { type: "image"; image: string | Uint8Array }
16
+ | { type: "tool_result"; toolResultId: string; content: string }
17
+ | { type: "tool_use"; tool: string; args: Record<string, unknown> };
18
+
19
+ /**
20
+ * Hooks configuration for skills
21
+ */
22
+ export interface HooksSettings {
23
+ "pre-task"?: string[];
24
+ "post-task"?: string[];
25
+ }
26
+
27
+ /**
28
+ * Frontmatter shell configuration
29
+ */
30
+ export interface FrontmatterShell {
31
+ name?: string;
32
+ command?: string;
33
+ env?: Record<string, string>;
34
+ }
35
+
36
+ /**
37
+ * Context passed to skill prompt generators
38
+ */
39
+ export interface ToolUseContext {
40
+ tools: Record<string, (...args: unknown[]) => unknown>;
41
+ env: Record<string, unknown>;
42
+ cwd: string;
43
+ }
44
+
45
+ /**
46
+ * Skill loaded from source
47
+ */
48
+ export type SkillLoadedFrom =
49
+ | "skills" // Loaded from skills directory
50
+ | "commands" // Loaded from commands directory (legacy)
51
+ | "plugin" // From a plugin
52
+ | "bundled" // Built-in bundled skill
53
+ | "mcp"; // From MCP server
54
+
55
+ /**
56
+ * Skill configuration source
57
+ */
58
+ export type SkillSource =
59
+ | "userSettings" // User's global skills (~/.config/koi/skills)
60
+ | "projectSettings" // Project-level skills (.claude/skills)
61
+ | "policySettings" // Policy-defined skills
62
+ | "plugin" // From a plugin
63
+ | "bundled" // Built-in bundled skill
64
+ | "mcp"; // From MCP server
65
+
66
+ /**
67
+ * Parsed frontmatter fields from SKILL.md
68
+ */
69
+ export interface ParsedFields {
70
+ name?: string;
71
+ description?: string | string[];
72
+ when_to_use?: string;
73
+ argument_hint?: string;
74
+ arguments?: string | string[];
75
+ allowed_tools?: string | string[];
76
+ model?: string;
77
+ disable_model_invocation?: boolean;
78
+ user_invocable?: boolean;
79
+ hooks?: HooksSettings;
80
+ context?: "fork" | "inline";
81
+ agent?: string;
82
+ effort?: string;
83
+ paths?: string | string[];
84
+ shell?: FrontmatterShell;
85
+ version?: string;
86
+ }
87
+
88
+ /**
89
+ * A skill command that can be invoked
90
+ */
91
+ export interface SkillCommand {
92
+ type: "prompt";
93
+ name: string;
94
+ description: string;
95
+ hasUserSpecifiedDescription: boolean;
96
+ allowedTools: string[];
97
+ argumentHint?: string;
98
+ argNames?: string[];
99
+ whenToUse?: string;
100
+ version?: string;
101
+ model?: string;
102
+ disableModelInvocation: boolean;
103
+ userInvocable: boolean;
104
+ context?: "fork" | "inline";
105
+ agent?: string;
106
+ effort?: string;
107
+ paths?: string[];
108
+ contentLength: number;
109
+ isHidden: boolean;
110
+ progressMessage: string;
111
+ source: SkillSource;
112
+ loadedFrom: SkillLoadedFrom;
113
+ hooks?: HooksSettings;
114
+ skillRoot?: string;
115
+ /**
116
+ * Generate the prompt content for this skill with given arguments
117
+ */
118
+ getPromptForCommand: (args: string, ctx: ToolUseContext) => Promise<ContentBlockParam[]>;
119
+ }
120
+
121
+ /**
122
+ * A skill with its source file path
123
+ */
124
+ export interface SkillWithPath {
125
+ skill: SkillCommand;
126
+ filePath: string;
127
+ }
128
+
129
+ /**
130
+ * Bundled skill definition (used for registration)
131
+ */
132
+ export interface BundledSkillDefinition {
133
+ name: string;
134
+ description: string;
135
+ aliases?: string[];
136
+ whenToUse?: string;
137
+ argumentHint?: string;
138
+ allowedTools?: string[];
139
+ model?: string;
140
+ disableModelInvocation?: boolean;
141
+ userInvocable?: boolean;
142
+ isEnabled?: () => boolean;
143
+ hooks?: HooksSettings;
144
+ context?: "inline" | "fork";
145
+ agent?: string;
146
+ effort?: string;
147
+ template?: string;
148
+ files?: Record<string, string>;
149
+ getPromptForCommand: (args: string, ctx: ToolUseContext) => Promise<ContentBlockParam[]>;
150
+ }
151
+
152
+ /**
153
+ * Skill invocation result
154
+ */
155
+ export interface SkillInvocationResult {
156
+ success: boolean;
157
+ content?: ContentBlockParam[];
158
+ error?: string;
159
+ }
160
+
161
+ /**
162
+ * Frontmatter parsed from a SKILL.md file
163
+ */
164
+ export interface SkillFrontmatter {
165
+ name?: string;
166
+ description?: string | string[];
167
+ when_to_use?: string;
168
+ "argument-hint"?: string;
169
+ arguments?: string | string[];
170
+ "allowed-tools"?: string | string[];
171
+ model?: string;
172
+ "disable-model-invocation"?: boolean;
173
+ "user-invocable"?: boolean;
174
+ hooks?: HooksSettings;
175
+ context?: "fork" | "inline";
176
+ agent?: string;
177
+ effort?: string;
178
+ paths?: string | string[];
179
+ shell?: FrontmatterShell;
180
+ version?: string;
181
+ }
182
+
183
+ /**
184
+ * Skill file info for discovery
185
+ */
186
+ export interface SkillFileInfo {
187
+ path: string;
188
+ name: string;
189
+ root: string;
190
+ source: SkillSource;
191
+ loadedFrom: SkillLoadedFrom;
192
+ }
193
+
194
+ /**
195
+ * Dynamic skill loading event
196
+ */
197
+ export interface SkillsLoadedEvent {
198
+ skills: SkillCommand[];
199
+ source: SkillSource;
200
+ timestamp: number;
201
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Agent Tool — Spawn a subagent to perform a focused task.
3
+ *
4
+ * Implements Claude Code's AgentTool semantics on top of Pi's agent framework:
5
+ * • Built-in types: "explore" (read-only), "plan" (read-only + tasks)
6
+ * • Synchronous mode: wait for completion, return result text
7
+ * • Asynchronous mode: fire-and-forget, return agentId immediately
8
+ *
9
+ * Subagents are intentionally isolated:
10
+ * • Fresh message history (only the prompt)
11
+ * • Filtered tool set (no nested agents, no user questions, no plan-mode exit)
12
+ * • Max turn limit to prevent runaway token consumption
13
+ */
14
+
15
+ import { Type } from "typebox";
16
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
17
+ import type { ToolResultWithError } from "./types.js";
18
+ import { activeSessionRef } from "../agent/hooks.js";
19
+ import { runSubagent, type SubagentConfig } from "../agent/subagent.js";
20
+ import { subagentRegistry } from "../agent/subagent-registry.js";
21
+
22
+ // ─── Schema ──────────────────────────────────────────────────────────────────
23
+
24
+ export const agentSchema = Type.Object({
25
+ description: Type.String({
26
+ description: "A short (3-5 word) description of the task",
27
+ maxLength: 60,
28
+ }),
29
+ prompt: Type.String({
30
+ description: "The complete task description for the subagent",
31
+ }),
32
+ subagent_type: Type.Optional(
33
+ Type.Union(
34
+ [Type.Literal("explore"), Type.Literal("plan")],
35
+ { description: "Built-in agent type: 'explore' = read-only, 'plan' = planning mode. Defaults to 'explore' if omitted." }
36
+ )
37
+ ),
38
+ model: Type.Optional(
39
+ Type.String({
40
+ description: "Model override (optional, not yet implemented)",
41
+ })
42
+ ),
43
+ run_in_background: Type.Optional(
44
+ Type.Boolean({
45
+ description: "Run asynchronously in the background. A <task-notification> will be injected into your context when it completes. The user does not see this notification.",
46
+ })
47
+ ),
48
+ });
49
+
50
+ export type AgentToolInput = {
51
+ description: string;
52
+ prompt: string;
53
+ subagent_type?: "explore" | "plan";
54
+ model?: string;
55
+ run_in_background?: boolean;
56
+ };
57
+
58
+ // ─── Execute ─────────────────────────────────────────────────────────────────
59
+
60
+ export async function executeAgent(
61
+ _toolCallId: string,
62
+ params: AgentToolInput
63
+ ): Promise<ToolResultWithError<{ result?: string; agentId?: string; status: string }>> {
64
+ if (!activeSessionRef.current) {
65
+ return {
66
+ content: [{ type: "text", text: "Error: No active session available to spawn subagent." }],
67
+ details: { status: "error" },
68
+ isError: true,
69
+ } as ToolResultWithError<{ result?: string; agentId?: string; status: string }>;
70
+ }
71
+
72
+ const config: SubagentConfig = {
73
+ description: params.description,
74
+ prompt: params.prompt,
75
+ subagentType: params.subagent_type ?? "explore",
76
+ runInBackground: params.run_in_background,
77
+ };
78
+
79
+ if (params.run_in_background) {
80
+ const agentId = await subagentRegistry.launch(config);
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Launched background agent ${agentId}: ${params.description}\n\nThe agent is running asynchronously. A <task-notification> will be injected into your context when it completes. The user does not see this notification.`,
86
+ },
87
+ ],
88
+ details: { status: "async_launched", agentId },
89
+ };
90
+ }
91
+
92
+ try {
93
+ const result = await runSubagent(config);
94
+ return {
95
+ content: [
96
+ { type: "text", text: result || "[Agent completed with empty output]" },
97
+ ],
98
+ details: { status: "completed", result },
99
+ };
100
+ } catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ return {
103
+ content: [{ type: "text", text: `Subagent failed: ${message}` }],
104
+ details: { status: "failed" },
105
+ isError: true,
106
+ } as ToolResultWithError<{ result?: string; agentId?: string; status: string }>;
107
+ }
108
+ }
109
+
110
+ // ─── Tool Definition ─────────────────────────────────────────────────────────
111
+
112
+ export function createAgentToolDefinition(): ToolDefinition<
113
+ typeof agentSchema,
114
+ { result?: string; agentId?: string; status: string }
115
+ > {
116
+ return {
117
+ name: "agent",
118
+ label: "Agent",
119
+ description:
120
+ "Spawn a focused subagent to perform a specific task in isolation.\n\n" +
121
+ "Use this tool to delegate parallelizable work (e.g., exploring a directory, " +
122
+ "researching a file, drafting a plan) while you continue with other tasks.\n\n" +
123
+ "Built-in types:\n" +
124
+ " • explore — read-only tools only; safe for research and discovery (default)\n" +
125
+ " • plan — read-only + task tools; for formulating implementation plans\n\n" +
126
+ "If subagent_type is omitted, it defaults to 'explore' for safety.\n" +
127
+ "Set run_in_background to true for fire-and-forget execution. " +
128
+ "You will receive a <task-notification> when the background agent completes.",
129
+ promptSnippet: "Agent: spawn a focused subagent to perform a task",
130
+ promptGuidelines: [
131
+ "Use Agent to delegate independent, parallelizable tasks.",
132
+ "Provide a concise description (3-5 words) and a detailed prompt.",
133
+ "Choose 'explore' for read-only research, 'plan' for drafting plans.",
134
+ "Set run_in_background when you don't need the result immediately.",
135
+ "Subagents cannot spawn other subagents or ask the user questions.",
136
+ ],
137
+ parameters: agentSchema,
138
+ executionMode: "parallel",
139
+ async execute(toolCallId, params, _signal, _onUpdate) {
140
+ return executeAgent(toolCallId, params);
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * AskUserQuestion Tool
3
+ *
4
+ * Allows the agent to ask the user a multiple-choice question.
5
+ * The question is shown in a modal dialog; the user's answer is returned.
6
+ * An "Other (custom)" option is always appended automatically.
7
+ */
8
+
9
+ import { Type } from "typebox";
10
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
11
+ import { askUserQuestion } from "../agent/question-ui.js";
12
+
13
+ export const askUserQuestionSchema = Type.Object({
14
+ question: Type.String({ description: "The question to ask the user" }),
15
+ options: Type.Array(Type.String(), {
16
+ description: "List of answer options for the user to choose from",
17
+ }),
18
+ });
19
+
20
+ export type AskUserQuestionToolInput = {
21
+ question: string;
22
+ options: string[];
23
+ };
24
+
25
+ export function createAskUserQuestionToolDefinition(): ToolDefinition {
26
+ return {
27
+ name: "askUserQuestion",
28
+ label: "Ask User Question",
29
+ description:
30
+ "Ask the user a multiple-choice question to clarify requirements or collect preferences. " +
31
+ "An additional 'Other (custom)' option is always provided automatically.",
32
+ parameters: askUserQuestionSchema,
33
+ executionMode: "parallel",
34
+ async execute(_toolCallId, params, _signal, _onUpdate) {
35
+ const input = params as AskUserQuestionToolInput;
36
+ const answer = await askUserQuestion({
37
+ question: input.question,
38
+ options: input.options,
39
+ });
40
+ return {
41
+ content: [{ type: "text", text: answer }],
42
+ details: { answer },
43
+ };
44
+ },
45
+ } as ToolDefinition;
46
+ }