mono-pilot 0.2.9 → 0.2.12

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 (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -1,7 +1,9 @@
1
- import { homedir } from "node:os";
2
- import { resolve } from "node:path";
1
+ import { keyHint } from "@mariozechner/pi-coding-agent";
2
+ import { Text } from "@mariozechner/pi-tui";
3
3
  import { Type } from "@sinclair/typebox";
4
- import { createRpcRequestId, extractStringHeaders, formatErrorMessage, formatJsonRpcError, inferTransport, isRecord, isServerEnabled, MCP_CONFIG_RELATIVE_PATH, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, initializeMcpSession, } from "../src/utils/mcp-client.js";
4
+ import { resolveTargetServers } from "../mcp/servers.js";
5
+ import { createRpcRequestId, formatJsonRpcError, initializeMcpSession, postJsonRpcRequest } from "../mcp/protocol.js";
6
+ import { formatErrorMessage, isRecord, toNonEmptyString } from "../mcp/config.js";
5
7
  const DESCRIPTION = `List available resources from configured MCP servers. Each returned resource will include all standard MCP resource fields plus a 'server' field indicating which server the resource belongs to. MCP resources are _not_ the same as tools, so don't call this function to discover MCP tools.`;
6
8
  const listMcpResourcesSchema = Type.Object({
7
9
  server: Type.Optional(Type.String({
@@ -49,12 +51,9 @@ async function listRemoteMcpResources(options) {
49
51
  mimeType: toNonEmptyString(raw.mimeType),
50
52
  });
51
53
  }
52
- return {
53
- resources,
54
- nextCursor: toNonEmptyString(resourcesBody.result.nextCursor),
55
- };
54
+ return { resources, nextCursor: toNonEmptyString(resourcesBody.result.nextCursor) };
56
55
  }
57
- function normalizeServerFilter(value) {
56
+ function normalizeOptionalString(value) {
58
57
  if (typeof value !== "string")
59
58
  return undefined;
60
59
  const normalized = value.trim();
@@ -66,62 +65,50 @@ export default function listMcpResourcesExtension(pi) {
66
65
  label: "ListMcpResources",
67
66
  description: DESCRIPTION,
68
67
  parameters: listMcpResourcesSchema,
69
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
70
- let serverFilter;
71
- try {
72
- serverFilter = normalizeServerFilter(params.server);
68
+ renderCall(args, theme) {
69
+ const input = args;
70
+ const server = typeof input.server === "string" && input.server.trim().length > 0 ? input.server : undefined;
71
+ let text = theme.fg("toolTitle", theme.bold("ListMcpResources"));
72
+ if (server)
73
+ text += ` ${theme.fg("toolOutput", server)}`;
74
+ return new Text(text, 0, 0);
75
+ },
76
+ renderResult(result, { expanded, isPartial }, theme) {
77
+ if (isPartial) {
78
+ return new Text(theme.fg("muted", "Listing resources..."), 0, 0);
73
79
  }
74
- catch (error) {
75
- const message = formatErrorMessage(error);
76
- return {
77
- content: [{ type: "text", text: message }],
78
- details: { error: message },
79
- isError: true,
80
- };
80
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
81
+ if (!textBlock) {
82
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
81
83
  }
82
- const configPath = resolveMcpConfigPath(ctx.cwd);
83
- if (!configPath) {
84
- const workspaceCandidate = resolve(ctx.cwd, MCP_CONFIG_RELATIVE_PATH);
85
- const homeCandidate = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
86
- const message = `MCP config not found. Checked:\n- ${workspaceCandidate}\n- ${homeCandidate}`;
87
- return {
88
- content: [{ type: "text", text: message }],
89
- details: { error: message },
90
- isError: true,
91
- };
84
+ const fullText = textBlock.text;
85
+ const details = result.details;
86
+ const count = details?.total_resources ?? 0;
87
+ if (!expanded) {
88
+ const summary = `${count} resources (click or ${keyHint("expandTools", "to expand")})`;
89
+ return new Text(theme.fg("muted", summary), 0, 0);
92
90
  }
93
- let servers;
91
+ let text = fullText.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
92
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
93
+ return new Text(text, 0, 0);
94
+ },
95
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
96
+ const serverFilter = normalizeOptionalString(params.server);
97
+ let targetServers;
98
+ let sources;
94
99
  try {
95
- servers = await parseMcpConfig(configPath);
100
+ const result = await resolveTargetServers(ctx.cwd, serverFilter);
101
+ targetServers = result.servers;
102
+ sources = result.sources;
96
103
  }
97
104
  catch (error) {
98
105
  const message = formatErrorMessage(error);
99
106
  return {
100
107
  content: [{ type: "text", text: message }],
101
- details: {
102
- config_path: configPath,
103
- error: message,
104
- },
108
+ details: { error: message },
105
109
  isError: true,
106
110
  };
107
111
  }
108
- const targetServers = [];
109
- for (const [serverName, serverConfig] of Object.entries(servers)) {
110
- if (serverFilter && serverName !== serverFilter)
111
- continue;
112
- if (!isServerEnabled(serverConfig))
113
- continue;
114
- if (inferTransport(serverConfig) !== "remote")
115
- continue;
116
- const serverUrl = toNonEmptyString(serverConfig.url);
117
- if (!serverUrl)
118
- continue;
119
- targetServers.push({
120
- name: serverName,
121
- url: serverUrl,
122
- headers: extractStringHeaders(serverConfig.headers),
123
- });
124
- }
125
112
  if (targetServers.length === 0) {
126
113
  const message = serverFilter
127
114
  ? `No active remote MCP server found matching '${serverFilter}'.`
@@ -129,13 +116,16 @@ export default function listMcpResourcesExtension(pi) {
129
116
  return {
130
117
  content: [{ type: "text", text: message }],
131
118
  details: {
132
- config_path: configPath,
119
+ config_paths: sources.map((s) => s.path),
133
120
  servers_matched: 0,
134
121
  },
135
122
  };
136
123
  }
137
124
  const lines = [];
138
- lines.push(`MCP config: ${configPath}`);
125
+ lines.push("MCP config:");
126
+ for (const source of sources) {
127
+ lines.push(`- ${source.scope}: ${source.path}`);
128
+ }
139
129
  lines.push(`Servers matched: ${targetServers.length}`);
140
130
  if (serverFilter)
141
131
  lines.push(`Server filter: ${serverFilter}`);
@@ -162,8 +152,7 @@ export default function listMcpResourcesExtension(pi) {
162
152
  if (resource.mimeType)
163
153
  lines.push(` mimeType: ${resource.mimeType}`);
164
154
  if (resource.description) {
165
- const descLines = resource.description.split("\n");
166
- for (const descLine of descLines) {
155
+ for (const descLine of resource.description.split("\n")) {
167
156
  lines.push(` ${descLine}`);
168
157
  }
169
158
  }
@@ -180,7 +169,7 @@ export default function listMcpResourcesExtension(pi) {
180
169
  return {
181
170
  content: [{ type: "text", text: lines.join("\n") }],
182
171
  details: {
183
- config_path: configPath,
172
+ config_paths: sources.map((s) => s.path),
184
173
  servers_matched: targetServers.length,
185
174
  servers_queried: targetServers.length,
186
175
  servers_failed: serversFailed,
@@ -1,7 +1,9 @@
1
- import { homedir } from "node:os";
2
- import { resolve } from "node:path";
1
+ import { keyHint } from "@mariozechner/pi-coding-agent";
2
+ import { Text } from "@mariozechner/pi-tui";
3
3
  import { Type } from "@sinclair/typebox";
4
- import { createRpcRequestId, extractStringHeaders, formatErrorMessage, formatJsonRpcError, inferTransport, isRecord, isServerEnabled, MCP_CONFIG_RELATIVE_PATH, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, initializeMcpSession, } from "../src/utils/mcp-client.js";
4
+ import { resolveTargetServers } from "../mcp/servers.js";
5
+ import { createRpcRequestId, formatJsonRpcError, initializeMcpSession, postJsonRpcRequest } from "../mcp/protocol.js";
6
+ import { formatErrorMessage, isRecord, toNonEmptyString } from "../mcp/config.js";
5
7
  const DESCRIPTION = `List available MCP tools from configured MCP servers. Each returned tool includes server metadata. If server is provided, results are limited to that server. If toolName is provided, returns full documentation and input JSON schema for matching tools.`;
6
8
  const listMcpToolsSchema = Type.Object({
7
9
  server: Type.Optional(Type.String({
@@ -44,16 +46,9 @@ async function listRemoteMcpTools(options) {
44
46
  const name = toNonEmptyString(raw.name);
45
47
  if (!name)
46
48
  continue;
47
- tools.push({
48
- name,
49
- description: toNonEmptyString(raw.description),
50
- inputSchema: raw.inputSchema,
51
- });
49
+ tools.push({ name, description: toNonEmptyString(raw.description), inputSchema: raw.inputSchema });
52
50
  }
53
- return {
54
- tools,
55
- nextCursor: toNonEmptyString(body.result.nextCursor),
56
- };
51
+ return { tools, nextCursor: toNonEmptyString(body.result.nextCursor) };
57
52
  }
58
53
  function normalizeOptionalString(value) {
59
54
  if (typeof value !== "string")
@@ -67,64 +62,57 @@ export default function listMcpToolsExtension(pi) {
67
62
  label: "ListMcpTools",
68
63
  description: DESCRIPTION,
69
64
  parameters: listMcpToolsSchema,
70
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
71
- let serverFilter;
72
- let toolNameFilter;
73
- try {
74
- serverFilter = normalizeOptionalString(params.server);
75
- toolNameFilter = normalizeOptionalString(params.toolName);
65
+ renderCall(args, theme) {
66
+ const input = args;
67
+ const commandArgs = [];
68
+ if (typeof input.server === "string" && input.server.trim()) {
69
+ commandArgs.push("--server", input.server.trim());
76
70
  }
77
- catch (error) {
78
- const message = formatErrorMessage(error);
79
- return {
80
- content: [{ type: "text", text: message }],
81
- details: { error: message },
82
- isError: true,
83
- };
71
+ if (typeof input.toolName === "string" && input.toolName.trim()) {
72
+ commandArgs.push("--toolName", input.toolName.trim());
84
73
  }
85
- const configPath = resolveMcpConfigPath(ctx.cwd);
86
- if (!configPath) {
87
- const workspaceCandidate = resolve(ctx.cwd, MCP_CONFIG_RELATIVE_PATH);
88
- const homeCandidate = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
89
- const message = `MCP config not found. Checked:\n- ${workspaceCandidate}\n- ${homeCandidate}`;
90
- return {
91
- content: [{ type: "text", text: message }],
92
- details: { error: message },
93
- isError: true,
94
- };
74
+ let text = theme.fg("toolTitle", theme.bold("ListMcpTools"));
75
+ if (commandArgs.length > 0)
76
+ text += ` ${theme.fg("toolOutput", commandArgs.join(" "))}`;
77
+ return new Text(text, 0, 0);
78
+ },
79
+ renderResult(result, { expanded, isPartial }, theme) {
80
+ if (isPartial) {
81
+ return new Text(theme.fg("muted", "Listing tools..."), 0, 0);
95
82
  }
96
- let servers;
83
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
84
+ if (!textBlock) {
85
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
86
+ }
87
+ const fullText = textBlock.text;
88
+ const details = result.details;
89
+ const count = details?.total_tools ?? 0;
90
+ if (!expanded) {
91
+ const summary = `${count} tools (click or ${keyHint("expandTools", "to expand")})`;
92
+ return new Text(theme.fg("muted", summary), 0, 0);
93
+ }
94
+ let text = fullText.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
95
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
96
+ return new Text(text, 0, 0);
97
+ },
98
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
99
+ const serverFilter = normalizeOptionalString(params.server);
100
+ const toolNameFilter = normalizeOptionalString(params.toolName);
101
+ let targetServers;
102
+ let sources;
97
103
  try {
98
- servers = await parseMcpConfig(configPath);
104
+ const result = await resolveTargetServers(ctx.cwd, serverFilter);
105
+ targetServers = result.servers;
106
+ sources = result.sources;
99
107
  }
100
108
  catch (error) {
101
109
  const message = formatErrorMessage(error);
102
110
  return {
103
111
  content: [{ type: "text", text: message }],
104
- details: {
105
- config_path: configPath,
106
- error: message,
107
- },
112
+ details: { error: message },
108
113
  isError: true,
109
114
  };
110
115
  }
111
- const targetServers = [];
112
- for (const [serverName, serverConfig] of Object.entries(servers)) {
113
- if (serverFilter && serverName !== serverFilter)
114
- continue;
115
- if (!isServerEnabled(serverConfig))
116
- continue;
117
- if (inferTransport(serverConfig) !== "remote")
118
- continue;
119
- const serverUrl = toNonEmptyString(serverConfig.url);
120
- if (!serverUrl)
121
- continue;
122
- targetServers.push({
123
- name: serverName,
124
- url: serverUrl,
125
- headers: extractStringHeaders(serverConfig.headers),
126
- });
127
- }
128
116
  if (targetServers.length === 0) {
129
117
  const message = serverFilter
130
118
  ? `No active remote MCP server found matching '${serverFilter}'.`
@@ -132,13 +120,16 @@ export default function listMcpToolsExtension(pi) {
132
120
  return {
133
121
  content: [{ type: "text", text: message }],
134
122
  details: {
135
- config_path: configPath,
123
+ config_paths: sources.map((s) => s.path),
136
124
  servers_matched: 0,
137
125
  },
138
126
  };
139
127
  }
140
128
  const lines = [];
141
- lines.push(`MCP config: ${configPath}`);
129
+ lines.push("MCP config:");
130
+ for (const source of sources) {
131
+ lines.push(`- ${source.scope}: ${source.path}`);
132
+ }
142
133
  lines.push(`Servers matched: ${targetServers.length}`);
143
134
  if (serverFilter)
144
135
  lines.push(`Server filter: ${serverFilter}`);
@@ -167,7 +158,6 @@ export default function listMcpToolsExtension(pi) {
167
158
  continue;
168
159
  }
169
160
  if (toolNameFilter) {
170
- // Detailed mode
171
161
  for (const tool of matchedTools) {
172
162
  totalTools++;
173
163
  lines.push(`## [${target.name}] ${tool.name}`);
@@ -187,7 +177,6 @@ export default function listMcpToolsExtension(pi) {
187
177
  }
188
178
  }
189
179
  else {
190
- // Summary mode
191
180
  lines.push(`Tools returned: ${matchedTools.length}`);
192
181
  lines.push("");
193
182
  for (const tool of matchedTools) {
@@ -218,7 +207,7 @@ export default function listMcpToolsExtension(pi) {
218
207
  return {
219
208
  content: [{ type: "text", text: lines.join("\n").trim() }],
220
209
  details: {
221
- config_path: configPath,
210
+ config_paths: sources.map((s) => s.path),
222
211
  servers_matched: targetServers.length,
223
212
  servers_queried: targetServers.length,
224
213
  servers_failed: serversFailed,
@@ -0,0 +1,48 @@
1
+ import { keyHint } from "@mariozechner/pi-coding-agent";
2
+ import { createLsTool } from "@mariozechner/pi-coding-agent";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ // Grab schema, description, label from the builtin ls tool
5
+ const builtinLs = createLsTool(process.cwd());
6
+ export default function (pi) {
7
+ pi.registerTool({
8
+ name: builtinLs.name,
9
+ label: builtinLs.label,
10
+ description: builtinLs.description,
11
+ parameters: builtinLs.parameters,
12
+ renderCall(args, theme) {
13
+ const input = args;
14
+ const dir = typeof input.path === "string" && input.path.trim().length > 0
15
+ ? input.path
16
+ : ".";
17
+ let text = theme.fg("toolTitle", theme.bold("ls"));
18
+ text += ` ${theme.fg("toolOutput", dir)}`;
19
+ return new Text(text, 0, 0);
20
+ },
21
+ renderResult(result, { expanded, isPartial }, theme) {
22
+ if (isPartial) {
23
+ return new Text(theme.fg("muted", "Listing..."), 0, 0);
24
+ }
25
+ const textBlock = result.content.find((entry) => entry.type === "text");
26
+ if (!textBlock) {
27
+ return new Text(theme.fg("error", "No output."), 0, 0);
28
+ }
29
+ const fullText = textBlock.text;
30
+ const entryCount = fullText.split("\n").filter(Boolean).length;
31
+ if (!expanded) {
32
+ const summary = `${entryCount} entries (click or ${keyHint("expandTools", "to expand")})`;
33
+ return new Text(theme.fg("muted", summary), 0, 0);
34
+ }
35
+ let text = fullText
36
+ .split("\n")
37
+ .map((line) => theme.fg("toolOutput", line))
38
+ .join("\n");
39
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
40
+ return new Text(text, 0, 0);
41
+ },
42
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
43
+ // Delegate to the built-in ls tool, constructed with the runtime cwd
44
+ const delegate = createLsTool(ctx.cwd);
45
+ return delegate.execute(toolCallId, params, signal, onUpdate);
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,67 @@
1
+ import { keyHint } from "@mariozechner/pi-coding-agent";
2
+ import { Text } from "@mariozechner/pi-tui";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { LSP } from "../lsp/index.js";
5
+ const DESCRIPTION = `Get LSP diagnostics (type errors, warnings, hints) for a specific file.
6
+
7
+ - Run after editing a file to verify no errors were introduced
8
+ - Provide the absolute path to the file
9
+ - Results formatted as: SEVERITY [line:col] message
10
+ - Returns "No diagnostics." if the file has no issues
11
+ - Language server is started automatically on first use
12
+ - Supported languages: TypeScript/JavaScript, Python (Pyright), Go (gopls), Rust (rust-analyzer), Swift (sourcekit-lsp)`;
13
+ const schema = Type.Object({
14
+ file: Type.String({
15
+ description: "Absolute path to the file to check for diagnostics.",
16
+ }),
17
+ });
18
+ export default function (pi) {
19
+ pi.registerTool({
20
+ name: "LspDiagnostics",
21
+ label: "LspDiagnostics",
22
+ description: DESCRIPTION,
23
+ parameters: schema,
24
+ renderCall(args, theme) {
25
+ const input = args;
26
+ const file = typeof input.file === "string" && input.file.trim().length > 0
27
+ ? input.file
28
+ : "(missing file)";
29
+ let text = theme.fg("toolTitle", theme.bold("LspDiagnostics"));
30
+ text += ` ${theme.fg("toolOutput", file)}`;
31
+ return new Text(text, 0, 0);
32
+ },
33
+ renderResult(result, { expanded, isPartial }, theme) {
34
+ if (isPartial) {
35
+ return new Text(theme.fg("muted", "Checking..."), 0, 0);
36
+ }
37
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
38
+ if (!textBlock) {
39
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
40
+ }
41
+ const fullText = textBlock.text;
42
+ const lineCount = fullText.split("\n").length;
43
+ const isClean = fullText === "No diagnostics.";
44
+ if (!expanded) {
45
+ const summary = isClean
46
+ ? theme.fg("muted", "No diagnostics.")
47
+ : `${theme.fg("error", `${lineCount} diagnostics`)} ${theme.fg("muted", `(click or ${keyHint("expandTools", "to expand")})`)}`;
48
+ return new Text(summary, 0, 0);
49
+ }
50
+ let text = fullText
51
+ .split("\n")
52
+ .map((line) => (isClean ? theme.fg("muted", line) : theme.fg("toolOutput", line)))
53
+ .join("\n");
54
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
55
+ return new Text(text, 0, 0);
56
+ },
57
+ async execute(_id, params, _signal, _onUpdate, _ctx) {
58
+ await LSP.touchFile(params.file, true);
59
+ const all = await LSP.diagnostics();
60
+ const diags = all[params.file] ?? [];
61
+ const text = diags.length === 0
62
+ ? "No diagnostics."
63
+ : diags.map((d) => LSP.Diagnostic.pretty(d)).join("\n");
64
+ return { content: [{ type: "text", text }], details: { file: params.file, count: diags.length } };
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,54 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { LSP } from "../lsp/index.js";
3
+ import { LspState } from "../lsp/runtime.js";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const DESCRIPTION = `Search for symbols (functions, classes, interfaces, etc.) across the entire workspace using LSP.
7
+
8
+ - More semantic than text search; understands code structure and filters to meaningful symbols only
9
+ - Provide a partial or full symbol name to search for
10
+ - Returns up to 10 matching symbols with their kind and file location
11
+ - Results formatted as: name (kind) relative/path/to/file.ts:line
12
+ - Supported languages: TypeScript/JavaScript, Python (Pyright), Go (gopls), Rust (rust-analyzer), Swift (sourcekit-lsp)`;
13
+ const KIND_NAMES = {
14
+ 5: "class",
15
+ 6: "method",
16
+ 10: "enum",
17
+ 11: "interface",
18
+ 12: "function",
19
+ 13: "variable",
20
+ 14: "constant",
21
+ 23: "struct",
22
+ };
23
+ const schema = Type.Object({
24
+ query: Type.String({
25
+ description: "Symbol name or partial name to search for.",
26
+ }),
27
+ });
28
+ export default function (pi) {
29
+ pi.registerTool({
30
+ name: "LspSymbols",
31
+ label: "LspSymbols",
32
+ description: DESCRIPTION,
33
+ parameters: schema,
34
+ async execute(_id, params, _signal, _onUpdate, _ctx) {
35
+ const symbols = await LSP.workspaceSymbol(params.query);
36
+ if (symbols.length === 0) {
37
+ return { content: [{ type: "text", text: "No symbols found." }], details: undefined };
38
+ }
39
+ const lines = symbols.map((s) => {
40
+ const kind = KIND_NAMES[s.kind] ?? `kind:${s.kind}`;
41
+ const uri = s.location?.uri ?? "";
42
+ const line = (s.location?.range?.start?.line ?? 0) + 1;
43
+ const file = uri.startsWith("file://")
44
+ ? path.relative(LspState.directory, fileURLToPath(uri))
45
+ : uri;
46
+ return `${s.name} (${kind}) ${file}:${line}`;
47
+ });
48
+ return {
49
+ content: [{ type: "text", text: lines.join("\n") }],
50
+ details: undefined,
51
+ };
52
+ },
53
+ });
54
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * MailBox tool — lets a game agent read queued bus messages.
3
+ */
4
+ import { keyHint } from "@mariozechner/pi-coding-agent";
5
+ import { Text } from "@mariozechner/pi-tui";
6
+ import { Type } from "@sinclair/typebox";
7
+ import { isMailBoxConnected, readMailBox, getMailBoxCount } from "../extensions/game/mailbox.js";
8
+ const mailboxSchema = Type.Object({
9
+ limit: Type.Optional(Type.Integer({ minimum: 1, description: "Max messages to return." })),
10
+ clear: Type.Optional(Type.Boolean({ description: "Clear returned messages (default: true)." })),
11
+ });
12
+ const mailboxExtension = (pi) => {
13
+ pi.registerTool({
14
+ name: "MailBox",
15
+ label: "MailBox",
16
+ description: "Read queued message-bus items that were not injected into the conversation. " +
17
+ "Use this to check public or private messages in game mode.",
18
+ parameters: mailboxSchema,
19
+ renderCall(args, theme) {
20
+ const input = args;
21
+ const limit = input.limit ? `limit=${input.limit}` : "all";
22
+ return new Text(`${theme.fg("toolTitle", theme.bold("mailbox"))} ${theme.fg("toolOutput", limit)}`, 0, 0);
23
+ },
24
+ renderResult(result, { expanded, isPartial }, theme) {
25
+ if (isPartial)
26
+ return new Text(theme.fg("muted", "Loading..."), 0, 0);
27
+ const text = result.content.find((e) => e.type === "text")?.text ?? "";
28
+ const details = result.details;
29
+ const count = details?.count;
30
+ const remaining = details?.remaining ?? 0;
31
+ if (!expanded && typeof count === "number") {
32
+ const extra = remaining > 0 ? `, ${remaining} unread` : "";
33
+ const summary = `${count} messages${extra} (click or ${keyHint("expandTools", "to expand")})`;
34
+ return new Text(theme.fg("muted", summary), 0, 0);
35
+ }
36
+ let body = text
37
+ .split("\n")
38
+ .map((line) => theme.fg("toolOutput", line))
39
+ .join("\n");
40
+ if (typeof count === "number") {
41
+ body += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
42
+ }
43
+ return new Text(body, 0, 0);
44
+ },
45
+ async execute(_toolCallId, params) {
46
+ if (!isMailBoxConnected()) {
47
+ return {
48
+ content: [{ type: "text", text: "MailBox not connected." }],
49
+ details: "not_connected",
50
+ };
51
+ }
52
+ const items = readMailBox({ limit: params.limit, clear: params.clear });
53
+ if (items.length === 0) {
54
+ return {
55
+ content: [{ type: "text", text: "MailBox empty." }],
56
+ details: "empty",
57
+ };
58
+ }
59
+ const lines = items.map((msg) => formatMessage(msg));
60
+ const remaining = getMailBoxCount();
61
+ const header = `MailBox (${items.length}):`;
62
+ const footer = remaining > 0 ? `\n(${remaining} more unread)` : "";
63
+ return {
64
+ content: [{ type: "text", text: `${header}\n${lines.join("\n")}${footer}` }],
65
+ details: { count: items.length, remaining },
66
+ };
67
+ },
68
+ });
69
+ };
70
+ function formatMessage(msg) {
71
+ const text = typeof msg.payload === "object" && msg.payload !== null && "text" in msg.payload
72
+ ? msg.payload.text
73
+ : JSON.stringify(msg.payload);
74
+ const sender = msg.fromName ?? msg.from;
75
+ const channel = msg.channel ?? "public";
76
+ const channelLabel = channel.startsWith("private:") ? "私信" : channel;
77
+ return [
78
+ `- from: ${sender}`,
79
+ ` seq: ${msg.seq}`,
80
+ ` channel: ${channelLabel}`,
81
+ ` message: ${text}`,
82
+ "",
83
+ ].join("\n");
84
+ }
85
+ export default mailboxExtension;