@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,73 @@
1
+ /**
2
+ * Koi Custom Tool Registry
3
+ *
4
+ * Re-implements Pi's built-in tools with custom orchestration and permission checks.
5
+ * All tools are registered via createAgentSession({ customTools, noTools: 'builtin' }).
6
+ */
7
+
8
+ import type { SessionTaskManager } from "../agent/session-tasks.js";
9
+ import { createReadToolDefinition } from "./read.js";
10
+ import { createGrepToolDefinition } from "./grep.js";
11
+ import { createGlobToolDefinition } from "./glob.js";
12
+ import { createLsToolDefinition } from "./ls.js";
13
+ import { createBashToolDefinition } from "./bash.js";
14
+ import { createEditToolDefinition } from "./edit.js";
15
+ import { createWriteToolDefinition } from "./write.js";
16
+ import { createWebFetchToolDefinition } from "./webfetch.js";
17
+ import { createAskUserQuestionToolDefinition } from "./ask-user-question.js";
18
+ import {
19
+ createEnterPlanModeToolDefinition,
20
+ createExitPlanModeToolDefinition,
21
+ } from "./plan-mode.js";
22
+ import { createAgentToolDefinition } from "./agent.js";
23
+ import {
24
+ createTaskCreateToolDefinition,
25
+ createTaskGetToolDefinition,
26
+ createTaskListToolDefinition,
27
+ createTaskUpdateToolDefinition,
28
+ } from "./task.js";
29
+ import {
30
+ createMonitorToolDefinition,
31
+ createCancelMonitorToolDefinition,
32
+ } from "./monitor.js";
33
+ import { createSkillToolDefinition } from "./skill.js";
34
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
35
+
36
+ export function createCodingToolDefinitions(
37
+ _cwd: string,
38
+ taskManager: SessionTaskManager
39
+ ): ToolDefinition[] {
40
+ return [
41
+ createReadToolDefinition(_cwd),
42
+ createGrepToolDefinition(_cwd),
43
+ createGlobToolDefinition(_cwd),
44
+ createLsToolDefinition(_cwd),
45
+ createBashToolDefinition(_cwd),
46
+ createEditToolDefinition(_cwd),
47
+ createWriteToolDefinition(_cwd),
48
+ createWebFetchToolDefinition(_cwd),
49
+ createAskUserQuestionToolDefinition(),
50
+ createEnterPlanModeToolDefinition(),
51
+ createExitPlanModeToolDefinition(),
52
+ createAgentToolDefinition(),
53
+ createTaskCreateToolDefinition(_cwd, taskManager),
54
+ createTaskGetToolDefinition(_cwd, taskManager),
55
+ createTaskListToolDefinition(_cwd, taskManager),
56
+ createTaskUpdateToolDefinition(_cwd, taskManager),
57
+ createMonitorToolDefinition(),
58
+ createCancelMonitorToolDefinition(),
59
+ createSkillToolDefinition(),
60
+ ] as ToolDefinition[];
61
+ }
62
+
63
+ export function createReadOnlyToolDefinitions(_cwd: string): ToolDefinition[] {
64
+ return [
65
+ createReadToolDefinition(_cwd),
66
+ createGrepToolDefinition(_cwd),
67
+ createGlobToolDefinition(_cwd),
68
+ createLsToolDefinition(_cwd),
69
+ createWebFetchToolDefinition(_cwd),
70
+ ] as ToolDefinition[];
71
+ }
72
+
73
+ export * from "./types.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * List MCP Resources Tool
3
+ *
4
+ * Lists all available resources from connected MCP servers.
5
+ */
6
+
7
+ import { getAllMcpResources } from "../services/mcp/index.js";
8
+
9
+ export interface ListMcpResourcesResult {
10
+ content: Array<{ type: "text"; text: string }>;
11
+ isError?: boolean;
12
+ }
13
+
14
+ export interface ListMcpResourcesArgs {
15
+ server?: string;
16
+ }
17
+
18
+ export async function listMcpResources(args: ListMcpResourcesArgs): Promise<ListMcpResourcesResult> {
19
+ try {
20
+ const allResources = getAllMcpResources();
21
+ let resources = allResources;
22
+
23
+ // Filter by server if specified
24
+ if (args.server) {
25
+ resources = allResources.filter((r) => r.server === args.server);
26
+ }
27
+
28
+ if (resources.length === 0) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: args.server
34
+ ? `No resources found on MCP server '${args.server}'`
35
+ : "No MCP resources available from any connected server",
36
+ },
37
+ ],
38
+ };
39
+ }
40
+
41
+ // Group resources by server
42
+ const byServer = new Map<string, typeof resources>();
43
+ for (const resource of resources) {
44
+ const existing = byServer.get(resource.server) ?? [];
45
+ existing.push(resource);
46
+ byServer.set(resource.server, existing);
47
+ }
48
+
49
+ const lines: string[] = [];
50
+ lines.push(`# MCP Resources (${resources.length} total)\n`);
51
+
52
+ for (const [serverName, serverResources] of byServer) {
53
+ lines.push(`## ${serverName} (${serverResources.length} resources)`);
54
+ for (const resource of serverResources) {
55
+ const name = resource.name ?? resource.uri;
56
+ const desc = resource.description
57
+ ? `\n ${resource.description}`
58
+ : "";
59
+ const mime = resource.mimeType ? ` [${resource.mimeType}]` : "";
60
+ lines.push(`- \`${name}\`${mime}${desc}`);
61
+ }
62
+ lines.push("");
63
+ }
64
+
65
+ return { content: [{ type: "text", text: lines.join("\n") }] };
66
+ } catch (error) {
67
+ const errorMessage =
68
+ error instanceof Error ? error.message : String(error);
69
+ return {
70
+ content: [{ type: "text", text: `Error listing MCP resources: ${errorMessage}` }],
71
+ isError: true,
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * LsTool — Directory listing
3
+ *
4
+ * Lists files and directories with basic metadata.
5
+ */
6
+
7
+ import { Type } from "typebox";
8
+ import { readdirSync, statSync, existsSync } from "fs";
9
+ import { resolve } from "path";
10
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
11
+ import type { TextContent } from "@mariozechner/pi-ai";
12
+ import { checkPermission } from "../agent/check-permissions.js";
13
+ import { requestPermission } from "../agent/permission-ui.js";
14
+ import type { ToolResultWithError } from "./types.js";
15
+
16
+ export const lsSchema = Type.Object({
17
+ path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
18
+ });
19
+
20
+ export type LsToolInput = {
21
+ path?: string;
22
+ };
23
+
24
+ export async function executeLs(params: LsToolInput): Promise<{ content: TextContent[]; details: { entryCount: number } }> {
25
+ const dirPath = params.path ? resolve(params.path) : process.cwd();
26
+
27
+ if (!existsSync(dirPath)) {
28
+ throw new Error(`Directory not found: ${params.path ?? "."}`);
29
+ }
30
+
31
+ const entries = readdirSync(dirPath);
32
+ const lines: string[] = [];
33
+
34
+ for (const name of entries) {
35
+ const full = resolve(dirPath, name);
36
+ try {
37
+ const s = statSync(full);
38
+ const type = s.isDirectory() ? "d" : s.isFile() ? "f" : "?";
39
+ const size = s.isFile() ? String(s.size) : "-";
40
+ lines.push(`${type}\t${size}\t${name}`);
41
+ } catch {
42
+ lines.push(`?\t-\t${name}`);
43
+ }
44
+ }
45
+
46
+ const text = lines.length ? lines.join("\n") : "(empty directory)";
47
+ return {
48
+ content: [{ type: "text", text }],
49
+ details: { entryCount: lines.length },
50
+ };
51
+ }
52
+
53
+ export function createLsToolDefinition(_cwd: string): ToolDefinition<typeof lsSchema, { entryCount: number }> {
54
+ return {
55
+ name: "ls",
56
+ label: "Ls",
57
+ description: "List files and directories in a given path.",
58
+ promptSnippet: "Ls: list directory contents",
59
+ parameters: lsSchema,
60
+ executionMode: "parallel",
61
+ async execute(_toolCallId, params, _signal, _onUpdate) {
62
+ const perm = checkPermission("ls", params);
63
+ if (perm.decision === "deny") {
64
+ const result: ToolResultWithError<{ entryCount: number }> = {
65
+ content: [{ type: "text", text: `Permission denied: ${perm.reason ?? "ls operation blocked"}` }],
66
+ details: { entryCount: 0 },
67
+ isError: true,
68
+ };
69
+ return result;
70
+ }
71
+ if (perm.decision === "ask") {
72
+ const allowed = await requestPermission({ toolName: "ls", args: params, reason: perm.reason ?? "Confirm directory listing" });
73
+ if (!allowed) {
74
+ const result: ToolResultWithError<{ entryCount: number }> = {
75
+ content: [{ type: "text", text: "User denied permission to list directory." }],
76
+ details: { entryCount: 0 },
77
+ isError: true,
78
+ };
79
+ return result;
80
+ }
81
+ }
82
+ return await executeLs(params);
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MCP Tool Adapter
3
+ */
4
+
5
+ import type { McpToolResult, SerializedTool } from "../services/mcp/types.js";
6
+ import { getMcpConnection } from "../services/mcp/index.js";
7
+
8
+ export interface KoiMcpTool {
9
+ name: string;
10
+ description: string;
11
+ inputSchema?: Record<string, unknown>;
12
+ serverName: string;
13
+ originalToolName: string;
14
+ }
15
+
16
+ export function createMcpToolDefinition(tool: SerializedTool): KoiMcpTool {
17
+ const parts = tool.name.split("__");
18
+ if (parts.length !== 3 || parts[0] !== "mcp") {
19
+ throw new Error(`Invalid MCP tool name format: ${tool.name}`);
20
+ }
21
+
22
+ const serverName = parts[1] ?? "";
23
+ const originalToolName = parts[2] ?? "";
24
+
25
+ if (!originalToolName) throw new Error("Tool name is required");
26
+
27
+ return {
28
+ name: tool.name,
29
+ description: tool.description ?? `MCP tool from ${serverName}: ${originalToolName}`,
30
+ inputSchema: tool.inputSchema as Record<string, unknown> | undefined,
31
+ serverName,
32
+ originalToolName,
33
+ };
34
+ }
35
+
36
+ export async function executeMcpToolCall(toolName: string, args: Record<string, unknown>): Promise<McpToolResult> {
37
+ const parts = toolName.split("__");
38
+ if (parts.length !== 3 || parts[0] !== "mcp") {
39
+ return { success: false, error: `Invalid MCP tool name: ${toolName}`, isError: true };
40
+ }
41
+
42
+ const serverName = parts[1] ?? "";
43
+ const originalToolName = parts[2] ?? "";
44
+
45
+ if (!originalToolName) {
46
+ return { success: false, error: "Tool name is required", isError: true };
47
+ }
48
+
49
+ const connection = getMcpConnection(serverName);
50
+ if (!connection || connection.status !== "connected") {
51
+ return { success: false, error: `MCP server '${serverName}' is not connected`, isError: true };
52
+ }
53
+
54
+ try {
55
+ const result = await connection.client.callTool({ name: originalToolName, arguments: args });
56
+ return { success: true, content: result.content as Array<{ type: string; text?: string; [key: string]: unknown }> };
57
+ } catch (error) {
58
+ const errorMessage = error instanceof Error ? error.message : String(error);
59
+ return { success: false, error: errorMessage, isError: true, content: [{ type: "text", text: errorMessage }] };
60
+ }
61
+ }
62
+
63
+ export function createMcpToolDefinitions(): KoiMcpTool[] {
64
+ const { getAllMcpTools } = require("../services/mcp/index.js") as { getAllMcpTools: () => SerializedTool[] };
65
+ const mcpTools = getAllMcpTools();
66
+ return mcpTools.map((tool: SerializedTool) => createMcpToolDefinition(tool));
67
+ }
68
+
69
+ export function formatMcpToolResult(result: McpToolResult): string {
70
+ if (!result.success) return `Error: ${result.error ?? "Unknown error"}`;
71
+ if (!result.content || result.content.length === 0) return "No content returned";
72
+ return result.content.map((block) => {
73
+ if (block.type === "text") return block.text ?? "";
74
+ return JSON.stringify(block);
75
+ }).join("\n");
76
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Monitor Tool — Background Process Watcher
3
+ *
4
+ * Creates a background monitor that watches command output and notifies
5
+ * the main agent via steer() (when busy) or prompt() (when idle).
6
+ *
7
+ * Notifications are sent as internal XML tags filtered from UI but visible to LLM.
8
+ */
9
+
10
+ import { Type } from "typebox";
11
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
12
+ import type { TextContent } from "@mariozechner/pi-ai";
13
+ import { checkPermission } from "../agent/check-permissions.js";
14
+ import { requestPermission } from "../agent/permission-ui.js";
15
+ import { monitorRegistry } from "../agent/monitor-registry.js";
16
+ import { activeSessionRef } from "../agent/hooks.js";
17
+ import type { ToolResultWithError } from "./types.js";
18
+
19
+ // ─── CreateMonitor ────────────────────────────────────────────────────────────
20
+
21
+ export const createMonitorSchema = Type.Object({
22
+ command: Type.String({
23
+ description:
24
+ "The bash command to run and monitor. " +
25
+ "The monitor will watch stdout/stderr and notify on each output line.",
26
+ }),
27
+ description: Type.Optional(
28
+ Type.String({
29
+ description: "Human-readable label for this monitor (shown in sidebar).",
30
+ })
31
+ ),
32
+ });
33
+
34
+ export type CreateMonitorInput = {
35
+ command: string;
36
+ description?: string;
37
+ };
38
+
39
+ // ─── CancelMonitor ────────────────────────────────────────────────────────────
40
+
41
+ export const cancelMonitorSchema = Type.Object({
42
+ monitorId: Type.String({
43
+ description: "The ID of the monitor to cancel.",
44
+ }),
45
+ });
46
+
47
+ export type CancelMonitorInput = {
48
+ monitorId: string;
49
+ };
50
+
51
+ // ─── Tool Definitions ─────────────────────────────────────────────────────────
52
+
53
+ export function createMonitorToolDefinition(): ToolDefinition<
54
+ typeof createMonitorSchema,
55
+ { monitorId: string }
56
+ > {
57
+ return {
58
+ name: "CreateMonitor",
59
+ label: "CreateMonitor",
60
+ description:
61
+ "Start a background process monitor that watches command output. " +
62
+ "Each line of output is sent as a notification to the main agent. " +
63
+ "Use CancelMonitor to stop a running monitor. " +
64
+ "Useful for watching log files, CI/CD pipelines, long-running scripts, or directory changes.",
65
+ promptSnippet: "Monitor: watch background command output",
66
+ parameters: createMonitorSchema,
67
+ executionMode: "parallel",
68
+ async execute(_toolCallId, params: CreateMonitorInput) {
69
+ // Permission check — same rules as bash
70
+ const perm = checkPermission("bash", { command: params.command });
71
+ if (perm.decision === "deny") {
72
+ const result: ToolResultWithError<{ monitorId: string }> = {
73
+ content: [
74
+ { type: "text", text: `Permission denied: ${perm.reason ?? "operation blocked"}` },
75
+ ],
76
+ details: { monitorId: "" },
77
+ isError: true,
78
+ };
79
+ return result;
80
+ }
81
+ if (perm.decision === "ask") {
82
+ const allowed = await requestPermission({
83
+ toolName: "bash",
84
+ args: { command: params.command },
85
+ reason: perm.reason ?? "Confirm background monitor command",
86
+ });
87
+ if (!allowed) {
88
+ const result: ToolResultWithError<{ monitorId: string }> = {
89
+ content: [{ type: "text", text: "User denied permission to start monitor." }],
90
+ details: { monitorId: "" },
91
+ isError: true,
92
+ };
93
+ return result;
94
+ }
95
+ }
96
+
97
+ const sessionId = activeSessionRef.current?.sessionId ?? "unknown";
98
+ const monitorId = monitorRegistry.launch(sessionId, params.command, params.description ?? "");
99
+
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: `Monitor started with ID: ${monitorId}\nCommand: ${params.command}`,
105
+ } satisfies TextContent,
106
+ ],
107
+ details: { monitorId },
108
+ };
109
+ },
110
+ };
111
+ }
112
+
113
+ export function createCancelMonitorToolDefinition(): ToolDefinition<
114
+ typeof cancelMonitorSchema,
115
+ { success: boolean; monitorId: string }
116
+ > {
117
+ return {
118
+ name: "CancelMonitor",
119
+ label: "CancelMonitor",
120
+ description:
121
+ "Cancel a running background monitor. " +
122
+ "The monitored process will be terminated (SIGTERM, then SIGKILL).",
123
+ promptSnippet: "CancelMonitor: stop a background monitor",
124
+ parameters: cancelMonitorSchema,
125
+ executionMode: "parallel",
126
+ async execute(_toolCallId, params: CancelMonitorInput) {
127
+ const existing = monitorRegistry.get(params.monitorId);
128
+ if (!existing) {
129
+ const result: ToolResultWithError<{ success: boolean; monitorId: string }> = {
130
+ content: [
131
+ {
132
+ type: "text",
133
+ text: `Monitor not found: ${params.monitorId}`,
134
+ } satisfies TextContent,
135
+ ],
136
+ details: { success: false, monitorId: params.monitorId },
137
+ isError: true,
138
+ };
139
+ return result;
140
+ }
141
+
142
+ const killed = monitorRegistry.kill(params.monitorId);
143
+
144
+ const content: TextContent[] = [
145
+ {
146
+ type: "text",
147
+ text: killed
148
+ ? `Monitor ${params.monitorId} cancelled.`
149
+ : `Monitor ${params.monitorId} could not be cancelled (process may have already exited).`,
150
+ },
151
+ ];
152
+
153
+ return {
154
+ content,
155
+ details: { success: killed, monitorId: params.monitorId },
156
+ };
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Plan Mode Tools
3
+ *
4
+ * enterPlanMode — switches the agent into Plan mode (disables write/edit/bash).
5
+ * exitPlanMode — submits a plan for user approval before switching back to Build mode.
6
+ */
7
+
8
+ import { Type } from "typebox";
9
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
10
+ import {
11
+ setAgentMode,
12
+ getAgentMode,
13
+ getActiveToolNamesForMode,
14
+ injectModeIntoSystemPrompt,
15
+ } from "../agent/mode.js";
16
+ import { activeSessionRef } from "../agent/hooks.js";
17
+ import { submitPlanForApproval } from "../agent/plan-ui.js";
18
+
19
+ export const enterPlanModeSchema = Type.Object({});
20
+
21
+ export type EnterPlanModeToolInput = Record<string, never>;
22
+
23
+ export const exitPlanModeSchema = Type.Object({
24
+ plan: Type.String({
25
+ description:
26
+ "The detailed plan to present for approval. Must include concrete steps before exiting Plan Mode.",
27
+ }),
28
+ });
29
+
30
+ export type ExitPlanModeToolInput = {
31
+ plan: string;
32
+ };
33
+
34
+ export function createEnterPlanModeToolDefinition(): ToolDefinition {
35
+ return {
36
+ name: "enterPlanMode",
37
+ label: "Enter Plan Mode",
38
+ description:
39
+ "Switch to Plan Mode. In Plan Mode, write/edit/bash tools are disabled so you can " +
40
+ "research and design a solution before making changes. Use exitPlanMode when ready.",
41
+ parameters: enterPlanModeSchema,
42
+ executionMode: "parallel",
43
+ async execute(_toolCallId, _params, _signal, _onUpdate) {
44
+ const before = getAgentMode();
45
+ if (before === "plan") {
46
+ return {
47
+ content: [{ type: "text", text: "Already in Plan Mode." }],
48
+ details: { mode: "plan" },
49
+ };
50
+ }
51
+ setAgentMode("plan");
52
+ const session = activeSessionRef.current;
53
+ if (session) {
54
+ session.setActiveToolsByName(getActiveToolNamesForMode("plan"));
55
+ injectModeIntoSystemPrompt(session, "plan");
56
+ setTimeout(() => {
57
+ void session.compact();
58
+ }, 0);
59
+ }
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text",
64
+ text:
65
+ "Entered Plan Mode. Write/edit/bash tools are now disabled. " +
66
+ "Research and design your solution, then call exitPlanMode with a detailed plan.",
67
+ },
68
+ ],
69
+ details: { mode: "plan" },
70
+ };
71
+ },
72
+ } as ToolDefinition;
73
+ }
74
+
75
+ export function createExitPlanModeToolDefinition(): ToolDefinition {
76
+ return {
77
+ name: "exitPlanMode",
78
+ label: "Exit Plan Mode",
79
+ description:
80
+ "Submit a plan and request approval to exit Plan Mode and return to Build Mode. " +
81
+ "If the user approves, Build Mode is restored. If rejected, stay in Plan Mode and revise.",
82
+ parameters: exitPlanModeSchema,
83
+ executionMode: "parallel",
84
+ async execute(_toolCallId, params, _signal, _onUpdate) {
85
+ const input = params as ExitPlanModeToolInput;
86
+ const before = getAgentMode();
87
+ if (before !== "plan") {
88
+ return {
89
+ content: [{ type: "text", text: "Not in Plan Mode. No action taken." }],
90
+ details: { approved: false },
91
+ };
92
+ }
93
+
94
+ const result = await submitPlanForApproval({ plan: input.plan });
95
+ if (result.approved) {
96
+ setAgentMode("build");
97
+ const session = activeSessionRef.current;
98
+ if (session) {
99
+ session.setActiveToolsByName(getActiveToolNamesForMode("build"));
100
+ injectModeIntoSystemPrompt(session, "build");
101
+ setTimeout(() => {
102
+ void session
103
+ .compact()
104
+ .catch(() => {})
105
+ .then(() => session.agent.continue());
106
+ }, 0);
107
+ }
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: "Plan approved. Exited Plan Mode and returned to Build Mode.",
113
+ },
114
+ ],
115
+ details: { approved: true },
116
+ };
117
+ } else {
118
+ const commentText = result.comment ? ` Comment: ${result.comment}` : "";
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text:
124
+ `Plan was rejected by the user.${commentText} You are still in Plan Mode. ` +
125
+ "Please revise the plan based on user feedback and try exitPlanMode again.",
126
+ },
127
+ ],
128
+ details: { approved: false, comment: result.comment },
129
+ isError: true,
130
+ };
131
+ }
132
+ },
133
+ } as ToolDefinition;
134
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Read MCP Resource Tool
3
+ */
4
+
5
+ import { getMcpConnection, getAllMcpResources } from "../services/mcp/index.js";
6
+
7
+ export interface ReadMcpResourceResult {
8
+ content: Array<{ type: "text"; text: string }>;
9
+ isError?: boolean;
10
+ }
11
+
12
+ export interface ReadMcpResourceArgs {
13
+ uri: string;
14
+ server?: string;
15
+ }
16
+
17
+ export async function readMcpResource(args: ReadMcpResourceArgs): Promise<ReadMcpResourceResult> {
18
+ const { uri, server: targetServer } = args;
19
+
20
+ try {
21
+ let resourceServer = targetServer;
22
+ let foundResource = false;
23
+
24
+ if (targetServer) {
25
+ const connection = getMcpConnection(targetServer);
26
+ if (!connection || connection.status !== "connected") {
27
+ return { content: [{ type: "text", text: `MCP server '${targetServer}' is not connected` }], isError: true };
28
+ }
29
+ const resources = getAllMcpResources().filter(r => r.server === targetServer);
30
+ foundResource = resources.some(r => r.uri === uri);
31
+ } else {
32
+ const allResources = getAllMcpResources();
33
+ const resource = allResources.find(r => r.uri === uri);
34
+ if (resource) {
35
+ resourceServer = resource.server;
36
+ foundResource = true;
37
+ }
38
+ }
39
+
40
+ if (!foundResource || !resourceServer) {
41
+ return { content: [{ type: "text", text: `Resource '${uri}' not found${targetServer ? ` on server '${targetServer}'` : ""}` }], isError: true };
42
+ }
43
+
44
+ const connection = getMcpConnection(resourceServer);
45
+ if (!connection || connection.status !== "connected") {
46
+ return { content: [{ type: "text", text: `Server '${resourceServer}' is not connected` }], isError: true };
47
+ }
48
+
49
+ const result = await connection.client.readResource({ uri });
50
+ if (!result.contents || result.contents.length === 0) {
51
+ return { content: [{ type: "text", text: "Resource is empty" }] };
52
+ }
53
+
54
+ const lines: string[] = [];
55
+ lines.push(`# Resource: ${uri}`);
56
+ lines.push(`Server: ${resourceServer}`);
57
+ lines.push("");
58
+
59
+ for (const content of result.contents) {
60
+ const c = content as { type?: string; text?: string; mimeType?: string; blob?: string };
61
+ if (c.type === "text") {
62
+ lines.push("```");
63
+ lines.push(c.text ?? "");
64
+ lines.push("```");
65
+ } else if (c.type === "image") {
66
+ lines.push(`[Image resource: ${c.mimeType ?? "unknown type"}]`);
67
+ } else if (c.type === "blob") {
68
+ lines.push(`[Binary resource: ${c.mimeType ?? "unknown type"}]`);
69
+ } else {
70
+ lines.push(JSON.stringify(content, null, 2));
71
+ }
72
+ }
73
+
74
+ return { content: [{ type: "text", text: lines.join("\n") }] };
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : String(error);
77
+ return { content: [{ type: "text", text: `Error reading MCP resource: ${errorMessage}` }], isError: true };
78
+ }
79
+ }